Merge branch 'main' into dev-lilin

This commit is contained in:
lilin 2026-07-13 13:47:38 +08:00
commit 2a27394b08
50 changed files with 4784 additions and 1314 deletions

View File

@ -15,7 +15,14 @@
*/
package com.yfd.platform.config;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
import com.yfd.platform.qgc_env.wq.service.EnvWqDataService;
import com.yfd.platform.qgc_job.service.IDwJobService;
import com.yfd.platform.qgc_job.service.IEqJobService;
import com.yfd.platform.qgc_job.service.IFhJobService;
import com.yfd.platform.system.domain.QuartzJob;
import com.yfd.platform.system.mapper.QuartzJobMapper;
import com.yfd.platform.utils.QuartzManage;
@ -42,6 +49,10 @@ public class JobRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(JobRunner.class);
private final QuartzJobMapper quartzJobMapper;
private final QuartzManage quartzManage;
private final EnvWqDataService envWqDataService;
private final IEqJobService eqJobService;
private final IDwJobService dwJobService;
private final IFhJobService fhJobService;
/**
* 项目启动时重新激活启用的定时任务
@ -55,5 +66,25 @@ public class JobRunner implements ApplicationRunner {
quartzJobMapper.selectList(new LambdaQueryWrapper<QuartzJob>().eq(QuartzJob::getStatus, "1"));
quartzJobs.forEach(quartzManage::addJob);
log.info("--------------------定时任务注入完成---------------------");
JobBaseAo ao = new JobBaseAo();
//如果没有传递时间则默认为上个月的开始到现在
if (null == ao.getEndTime()){
ao.setEndTime(DateUtil.date());
}
if (null == ao.getStartTime()){
ao.setStartTime(DateUtil.beginOfYear(DateUtil.offset(ao.getEndTime(), DateField.YEAR,-1)));
}
envWqDataService.wqLastData(ao);
log.info("--------------------wq最新一条数据---------------------");
eqJobService.eqAnchorPointLastDate(ao);
log.info("--------------------eq最新一条数据---------------------");
eqJobService.eqAnchorPointLastDate(ao);
log.info("--------------------eq最新一条数据---------------------");
dwJobService.wtLastData(ao);
log.info("--------------------wt最新一条数据---------------------");
dwJobService.wtvtPointData(ao);
log.info("--------------------wt最新一条数据---------------------");
fhJobService.zqLastData(ao);
log.info("--------------------fh最新一条数据---------------------");
}
}

View File

@ -1,5 +1,6 @@
package com.yfd.platform.qgc_base.domain.vo;
import com.yfd.platform.qgc_eng.eq.entity.vo.LastTmEngEqDataVo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -99,4 +100,7 @@ public class EngPointVo {
@Schema(description = "基地排序")
private Integer baseStepSort;
@Schema(description="电站最新数据:入库流量和出库流量")
private LastTmEngEqDataVo lastTmEngEqDataVo;
}

View File

@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@ -42,8 +43,10 @@ import com.yfd.platform.qgc_base.mapper.SdEngInfoBHMapper;
import com.yfd.platform.qgc_base.service.IDataScopeFilterService;
import com.yfd.platform.qgc_base.service.IMsOperationLogService;
import com.yfd.platform.qgc_base.service.ISdEngInfoBHService;
import com.yfd.platform.qgc_eng.eq.entity.vo.LastTmEngEqDataVo;
import com.yfd.platform.system.service.IAdminAuthService;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import com.yfd.platform.utils.RedisCacheUtil;
import com.yfd.platform.utils.SecurityUtils;
import jakarta.annotation.Resource;
import org.springframework.cache.annotation.CacheEvict;
@ -65,7 +68,8 @@ import java.util.stream.Collectors;
public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEngInfoBH> implements ISdEngInfoBHService {
private static final Map<String, String> ENG_CODE_NAME_FIELD_MAP = new LinkedHashMap<>();
@Resource
private RedisCacheUtil redisCacheUtil;
@Resource
private IDataScopeFilterService dataScopeFilterService;
@ -1009,6 +1013,18 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEng
@Override
public EngStInfoResultVo getStcdInfo(String stcd) {
if (StrUtil.isBlank(stcd)) {
return null;
}
Map<String, Object> stInfoMap = microservicDynamicSQLMapper.getOneBySql(buildStcdInfoViewSql(), Collections.singletonMap("stcd", stcd));
if (stInfoMap != null && !stInfoMap.isEmpty()) {
EngStInfoResultVo result = new EngStInfoResultVo();
result.setMsStbprpT(buildCamelCaseMsStbprpTMap(stInfoMap));
result.setStBaseInfo(buildStBaseInfo(stInfoMap));
return result;
}
EngBaseInfoVo baseInfo = getStInfoByStcd(stcd);
if (baseInfo == null) {
return null;
@ -1035,6 +1051,138 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEng
return result;
}
private String buildStcdInfoViewSql() {
return "SELECT " +
"t.STCD AS stcd, " +
"t.ENNM AS ennm, " +
"t.STNM AS stnm, " +
"t.STTP AS sttp, " +
"t.STTP_CODE AS sttpCode, " +
"t.STTP_NAME AS sttpName, " +
"t.STTP_FULL_PATH AS sttpFullPath, " +
"t.STTP_TREE_LEVEL AS sttpTreeLevel, " +
"t.RSTCD AS rstcd, " +
"t.BASE_ID AS baseId, " +
"t.BASE_NAME AS baseName, " +
"t.HBRVCD AS hbrvcd, " +
"t.HBRVCD_NAME AS hbrvcdName, " +
"t.RVCD AS rvcd, " +
"t.RVCD_NAME AS rvcdName, " +
"t.ADDVCD AS addvcd, " +
"t.ADDVNM AS addvcdName, " +
"t.HYCD AS hycd, " +
"t.HYNM AS hynm, " +
"t.TOP_HYCD AS topHycd, " +
"t.TOP_HYNM AS topHynm, " +
"t.LGTD AS lgtd, " +
"t.LTTD AS lttd, " +
"t.STLC AS stlc, " +
"t.USFL AS usfl, " +
"t.DTIN AS dtin, " +
"t.DTIN_TM AS dtinTm, " +
"t.BLDSTT_CODE AS bldsttCode, " +
"CASE WHEN t.BLDSTT_CODE =2 THEN '已建' WHEN t.BLDSTT_CODE =1 THEN '在建' ELSE '未建/规划' END AS bldsttCcodeName, " +
"t.INTRODUCE AS introduce, " +
"t.LOGO AS logo, " +
"t.INFFILE AS inffile, " +
"t.INV AS inv, " +
"t.INVINMN AS invinmn, " +
"t.STDSDT AS stdsdt, " +
"t.PSTSTDT AS pststdt, " +
"t.PESSTDT AS pesstdt, " +
"t.SWDT AS swdt, " +
"t.JCDT AS jcdt, " +
"t.WDDT AS wddt, " +
"t.ORDER_INDEX AS orderIndex, " +
"t.REMARK AS remark, " +
"t.VLSR AS vlsr, " +
"t.VLSR_TM AS vlsrTm, " +
"t.RECORD_USER AS recordUser, " +
"t.RECORD_TIME AS recordTime, " +
"t.MODIFY_USER AS modifyUser, " +
"t.MODIFY_TIME AS modifyTime, " +
"t.IS_DELETED AS isDeleted, " +
"t.DELETE_USER AS deleteUser, " +
"t.DELETE_TIME AS deleteTime, " +
"t.DATA_SOURCE AS dataSource " +
"FROM V_MS_STBPRP_T t " +
"WHERE t.STCD = #{map.stcd}";
}
private EngStBaseInfoVo buildStBaseInfo(Map<String, Object> stInfoMap) {
EngStBaseInfoVo stBaseInfo = new EngStBaseInfoVo();
stBaseInfo.setStcd(getMapString(stInfoMap, "stcd"));
stBaseInfo.setStnm(getMapString(stInfoMap, "stnm"));
stBaseInfo.setSttpCode(getMapString(stInfoMap, "sttpCode"));
stBaseInfo.setSttpName(getMapString(stInfoMap, "sttpName"));
stBaseInfo.setSttpFullPath(getMapString(stInfoMap, "sttpFullPath"));
stBaseInfo.setSttpTreeLevel(getMapInteger(stInfoMap, "sttpTreeLevel"));
stBaseInfo.setStCode(null);
stBaseInfo.setStName(null);
stBaseInfo.setBaseId(getMapString(stInfoMap, "baseId"));
stBaseInfo.setBaseName(getMapString(stInfoMap, "baseName"));
stBaseInfo.setHbrvcd(getMapString(stInfoMap, "hbrvcd"));
stBaseInfo.setHbrvcdName(getMapString(stInfoMap, "hbrvcdName"));
stBaseInfo.setRvcd(getMapString(stInfoMap, "rvcd"));
stBaseInfo.setRvcdName(getMapString(stInfoMap, "rvcdName"));
return stBaseInfo;
}
private Map<String, Object> buildCamelCaseMsStbprpTMap(Map<String, Object> stInfoMap) {
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
List<String> canonicalKeys = Arrays.asList(
"stcd", "stnm","ennm", "sttp", "sttpCode", "sttpName", "sttpFullPath", "sttpTreeLevel",
"rstcd", "baseId", "baseName", "hbrvcd", "hbrvcdName", "rvcd", "rvcdName", "addvcd","addvcdName","hycd","hynm","topHycd","topHynm",
"lgtd", "lttd", "stlc", "usfl", "dtin", "dtinTm", "bldsttCode", "introduce", "logo",
"inffile","bldsttCcodeName", "inv", "invinmn", "stdsdt", "pststdt", "pesstdt", "swdt", "jcdt", "wddt",
"orderIndex", "remark", "vlsr", "vlsrTm", "recordUser", "recordTime", "modifyUser",
"modifyTime", "isDeleted", "deleteUser", "deleteTime", "dataSource"
);
for (String canonicalKey : canonicalKeys) {
result.put(canonicalKey, getMapValue(stInfoMap, canonicalKey));
}
return result;
}
private String getMapString(Map<String, Object> source, String key) {
Object value = getMapValue(source, key);
return value == null ? null : String.valueOf(value);
}
private Integer getMapInteger(Map<String, Object> source, String key) {
Object value = getMapValue(source, key);
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.intValue();
}
return Integer.parseInt(String.valueOf(value));
}
private Object getMapValue(Map<String, Object> source, String key) {
if (source == null || source.isEmpty() || StrUtil.isBlank(key)) {
return null;
}
if (source.containsKey(key)) {
return source.get(key);
}
String upperKey = key.toUpperCase(Locale.ROOT);
if (source.containsKey(upperKey)) {
return source.get(upperKey);
}
String lowerKey = key.toLowerCase(Locale.ROOT);
if (source.containsKey(lowerKey)) {
return source.get(lowerKey);
}
for (Map.Entry<String, Object> entry : source.entrySet()) {
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue();
}
}
return null;
}
private String buildVmsstbprptViewSql() {
return "SELECT " +
"eng.STCD AS id, " +
@ -2713,9 +2861,9 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEng
.append("t.LGTD AS lgtd, ")
.append("t.LTTD AS lttd, ")
.append("t.AVQ AS avq, ")
.append("t.YRGEB AS ttpwr, ")
.append("p.TTPWR AS ttpwr, ")
.append("t.FSLTDZ AS normz, ")
.append("t.YRGEB AS yrge, ")
.append("p.YRGE AS yrge, ")
.append("t.BASE_ID AS baseId, ")
.append("t.JCDT AS jcdt, ")
.append("t.ADDVCD_NAME AS addvcdName, ")
@ -2744,6 +2892,7 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEng
.append(" WHEN t.PRSC IN ('3') THEN 2 ")
.append(" END AS endInstalledType ")
.append("FROM SD_ENGINFO_B_H t ")
.append("LEFT JOIN SD_PWR_B_H p ON t.STCD = p.STCD AND NVL(p.IS_DELETED, 0) = 0 ")
.append("LEFT JOIN SD_HYDROBASE hb ON hb.BASEID = t.BASE_ID AND NVL(hb.IS_DELETED, 0) = 0 ")
.append("LEFT JOIN MS_ALONG_B along ON along.RVCD = t.HBRVCD AND along.CODE = 'common' AND NVL(along.IS_DELETED, 0) = 0 ")
.append("LEFT JOIN ( ")
@ -2785,7 +2934,24 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl<SdEngInfoBHMapper, SdEng
DataSourceLoadOptionsBase loadOptions = dataSourceRequest.toDevRequest();
Page<?> page = loadOptions == null ? null : QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake());
List<EngPointVo> list = microservicDynamicSQLMapper.pageAllListWithResultType(page, sql.toString(), paramMap, EngPointVo.class);
if(CollUtil.isNotEmpty(list)){
for( EngPointVo engPointVo: list ){
String stcd = engPointVo.getStcd();
//查询最新一条数据的缓存
LastTmEngEqDataVo lastTmEngEqDataVo = redisCacheUtil.fromJson(redisCacheUtil.getString("eqAnchorPointLastDate:"+stcd),LastTmEngEqDataVo.class);
if (ObjectUtil.isEmpty(lastTmEngEqDataVo)){
lastTmEngEqDataVo = new LastTmEngEqDataVo();
lastTmEngEqDataVo.setStcd(engPointVo.getStcd());
}else{
lastTmEngEqDataVo.setStcd(engPointVo.getStcd());
}
// 设置电站名称装机容量正常蓄水位
lastTmEngEqDataVo.setStnm(engPointVo.getEnnm());
lastTmEngEqDataVo.setTtpwr(engPointVo.getTtpwr());
lastTmEngEqDataVo.setNormz(engPointVo.getNormz());
engPointVo.setLastTmEngEqDataVo(lastTmEngEqDataVo);
}
}
result.setData(list);
result.setTotal(page == null ? list.size() : page.getTotal());
return result;

View File

@ -37,6 +37,7 @@ import com.yfd.platform.qgc_eng.eq.entity.vo.WbsbVo;
import com.yfd.platform.qgc_eng.eq.entity.vo.StBaseInfo;
import com.yfd.platform.qgc_eng.eq.entity.vo.StcdInfoVo;
import com.yfd.platform.qgc_eng.eq.service.EngEqDataService;
import com.yfd.platform.qgc_env.wq.entity.vo.MsWarnRuleDetailBVo;
import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import jakarta.annotation.Resource;
@ -6000,6 +6001,20 @@ public class EngEqDataServiceImpl implements EngEqDataService {
resultList.add(eqdsVo);
}
Map<String, Date> qecLimitQueryMap = new LinkedHashMap<>();
Date currentDate = new Date();
for (EngEqdsVo eqdsVo : resultList) {
if (eqdsVo == null || StrUtil.isBlank(eqdsVo.getRstcd()) || qecLimitQueryMap.containsKey(eqdsVo.getRstcd())) {
continue;
}
qecLimitQueryMap.put(eqdsVo.getRstcd(), currentDate);
}
Map<String, MsWarnRuleDetailBVo> qecLimitMap = getQecLimitMap(qecLimitQueryMap);
for (EngEqdsVo eqdsVo : resultList) {
MsWarnRuleDetailBVo ruleDetail = qecLimitMap.get(eqdsVo.getRstcd());
eqdsVo.setQecLimit(ruleDetail == null ? null : ruleDetail.getMinVal());
}
SiteAvoidanceUtils.calcSiteMapLev(resultList,
EngEqdsVo::getStcd,
EngEqdsVo::getLgtd,
@ -6011,4 +6026,152 @@ public class EngEqDataServiceImpl implements EngEqDataService {
dataSourceResult.setTotal(page == null ? resultList.size() : page.getTotal());
return dataSourceResult;
}
private Map<String, MsWarnRuleDetailBVo> getQecLimitMap(Map<String, Date> stcdTmMap) {
Map<String, MsWarnRuleDetailBVo> resultMap = new HashMap<>();
if (stcdTmMap == null || stcdTmMap.isEmpty()) {
return resultMap;
}
Map<String, Object> paramMap = new HashMap<>();
StringBuilder sql = new StringBuilder();
sql.append("SELECT ")
.append("rel.STCD AS stcd, ")
.append("det.ID AS id, ")
.append("det.RULE_ID AS ruleId, ")
.append("det.TB_ID AS tbId, ")
.append("det.YS AS ys, ")
.append("det.TM AS tm, ")
.append("det.BEGIN_TIME AS beginTime, ")
.append("det.END_TIME AS endTime, ")
.append("det.MIN_VAL AS minVal, ")
.append("det.MAX_VAL AS maxVal, ")
.append("det.VLSR AS vlsr, ")
.append("det.VLSR_TM AS vlsrTm ")
.append("FROM MS_WARN_RULE_STBPRP_B rel ")
.append("INNER JOIN MS_WARN_RULE_B rule ON rule.ID = rel.RULE_ID ")
.append(" AND rule.RULE_TYPE = 'EQMN' ")
.append(" AND NVL(rule.IS_DELETED, 0) = 0 ")
.append("INNER JOIN MS_WARN_RULE_DETAIL_B det ON det.RULE_ID = rule.ID ")
.append(" AND det.YS = 'QEC' ")
.append(" AND NVL(det.IS_DELETED, 0) = 0 ")
.append("INNER JOIN ST_TB_B tb ON tb.ID = det.TB_ID ")
.append(" AND tb.TB_CODE = 'QEC_R' ")
.append(" AND NVL(tb.IS_DELETED, 0) = 0 ")
.append("WHERE NVL(rel.IS_DELETED, 0) = 0 ");
appendInCondition(sql, paramMap, "rel.STCD", stcdTmMap.keySet(), "qecLimitStcd");
sql.append("ORDER BY rel.STCD, det.TM DESC, LPAD(det.BEGIN_TIME, 4, '0') DESC NULLS LAST, ")
.append("LPAD(det.END_TIME, 4, '0') DESC NULLS LAST");
List<Map<String, Object>> ruleRows = microservicDynamicSQLMapper.pageAllList(null, sql.toString(), paramMap);
if (CollUtil.isEmpty(ruleRows)) {
return resultMap;
}
Map<String, List<MsWarnRuleDetailBVo>> groupedRuleMap = new HashMap<>();
for (Map<String, Object> row : ruleRows) {
String stcd = asString(row.get("STCD"));
if (StrUtil.isBlank(stcd)) {
continue;
}
groupedRuleMap.computeIfAbsent(stcd, key -> new ArrayList<>()).add(toMsWarnRuleDetailBVo(row));
}
for (Map.Entry<String, Date> entry : stcdTmMap.entrySet()) {
MsWarnRuleDetailBVo detail = resolveQecLimitDetail(entry.getValue(), groupedRuleMap.get(entry.getKey()));
if (detail != null) {
resultMap.put(entry.getKey(), detail);
}
}
return resultMap;
}
private MsWarnRuleDetailBVo resolveQecLimitDetail(Date queryDate, List<MsWarnRuleDetailBVo> ruleRows) {
if (queryDate == null || CollUtil.isEmpty(ruleRows)) {
return null;
}
Date currentTm = null;
List<MsWarnRuleDetailBVo> currentRows = new ArrayList<>();
String monthDay = DateTimeFormatter.ofPattern("MMdd").format(
queryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
);
for (MsWarnRuleDetailBVo row : ruleRows) {
Date tm = row.getTm();
if (tm == null || queryDate.compareTo(tm) < 0) {
continue;
}
if (currentTm != null && !currentTm.equals(tm)) {
return matchQecLimitDetail(monthDay, currentRows);
}
currentTm = tm;
currentRows.add(row);
}
if (currentTm == null) {
return null;
}
return matchQecLimitDetail(monthDay, currentRows);
}
private MsWarnRuleDetailBVo matchQecLimitDetail(String monthDay, List<MsWarnRuleDetailBVo> ruleRows) {
MsWarnRuleDetailBVo defaultValue = null;
for (MsWarnRuleDetailBVo row : ruleRows) {
if (row == null || row.getMinVal() == null) {
continue;
}
String beginTime = normalizeMonthDay(row.getBeginTime());
String endTime = normalizeMonthDay(row.getEndTime());
if (StrUtil.isNotBlank(beginTime) && StrUtil.isNotBlank(endTime)) {
if (isMonthDayInRange(monthDay, beginTime, endTime)) {
return row;
}
continue;
}
if (StrUtil.isBlank(beginTime) && StrUtil.isBlank(endTime)) {
defaultValue = row;
}
}
return defaultValue;
}
private MsWarnRuleDetailBVo toMsWarnRuleDetailBVo(Map<String, Object> row) {
MsWarnRuleDetailBVo vo = new MsWarnRuleDetailBVo();
vo.setId(asString(row.get("ID")));
vo.setRuleId(asString(row.get("RULEID")));
vo.setTbId(asString(row.get("TBID")));
vo.setYs(asString(row.get("YS")));
vo.setTm(toDate(row.get("TM")));
vo.setBeginTime(asString(row.get("BEGINTIME")));
vo.setEndTime(asString(row.get("ENDTIME")));
vo.setMinVal(toBigDecimal(row.get("MINVAL")));
vo.setMaxVal(toBigDecimal(row.get("MAXVAL")));
vo.setVlsr(asString(row.get("VLSR")));
vo.setVlsrTm(toDate(row.get("VLSRTM")));
return vo;
}
private void appendInCondition(StringBuilder sql,
Map<String, Object> paramMap,
String column,
Iterable<String> values,
String prefix) {
if (values == null) {
return;
}
List<String> placeholders = new ArrayList<>();
int index = 0;
for (String value : values) {
if (StrUtil.isBlank(value)) {
continue;
}
String key = prefix + index++;
paramMap.put(key, value.trim());
placeholders.add("#{map." + key + "}");
}
if (CollUtil.isEmpty(placeholders)) {
return;
}
sql.append(" AND ").append(column).append(" IN (")
.append(String.join(", ", placeholders))
.append(") ");
}
}

View File

@ -4626,7 +4626,7 @@ public class FbStationServiceImpl implements FbStationService {
.append("a.RSTCD AS rstcds, ")
.append("a.BASE_ID AS baseId, ")
.append("a.DTIN AS dtin, ")
.append("eng.ENNM AS ennm, ")
.append("COALESCE(eng.ENNM, '') AS ennm, ")
.append("eng.RVCD AS rvcd, ")
.append("eng.ADDVCD AS addvcd, ")
.append("NVL(siteSort.SORT, 999999) AS siteStepSort, ")

View File

@ -22,6 +22,7 @@ import com.yfd.platform.qgc_env.warn.entity.vo.StcdWarnStateVo;
import com.yfd.platform.qgc_env.warn.service.WarnDataService;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import jakarta.annotation.Resource;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@ -260,6 +261,7 @@ public class WarnDataServiceImpl implements WarnDataService {
}
@Override
@Cacheable(cacheNames = "warn#3600", keyGenerator = "cacheKeyGenerator")
public DataSourceResult<AlarmPointVo> getAlarmPointKendoListCust(DataSourceRequest dataSourceRequest) {
DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest();
String startTimeText = loadOptions == null ? null : QgcQueryWrapperUtil.getFilterFieldValue(loadOptions, "startTime");

View File

@ -0,0 +1,310 @@
package com.yfd.platform.qgc_env.wq.entity;
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.math.BigDecimal;
import java.util.Date;
@Data
@TableName("SD_WQ_R")
@Schema(description = "水质监测数据表实体")
public class SdWqR {
@TableId(value = "ID", type = IdType.ASSIGN_UUID)
@Schema(description = "主键")
private String id;
@TableField("STCD")
@Schema(description = "站码")
private String stcd;
@TableField("TM")
@Schema(description = "采样时间")
private Date tm;
@TableField("WQGRD")
@Schema(description = "水质类别")
private String wqgrd;
@Schema(description = "水质类别")
@TableField(exist = false)
private String wqgrdName;
@TableField("PERIOD")
@Schema(description = "丰平枯时期")
private String period;
@TableField("SPSTLC")
@Schema(description = "水平位置")
private Long spstlc;
@TableField("WTMP")
@Schema(description = "水温")
private BigDecimal wtmp;
@TableField("PH")
@Schema(description = "PH")
private BigDecimal ph;
@TableField("DOX")
@Schema(description = "溶解氧")
private BigDecimal dox;
@TableField("CODMN")
@Schema(description = "高锰酸盐指数")
private BigDecimal codmn;
@TableField("CODCR")
@Schema(description = "化学需氧量")
private BigDecimal codcr;
@TableField("BOD5")
@Schema(description = "五日生化需氧量")
private BigDecimal bod5;
@TableField("NH3N")
@Schema(description = "氨氮")
private BigDecimal nh3n;
@TableField("TP")
@Schema(description = "总磷")
private BigDecimal tp;
@TableField("TN")
@Schema(description = "总氮")
private BigDecimal tn;
@TableField("CU")
@Schema(description = "")
private BigDecimal cu;
@TableField("ZN")
@Schema(description = "")
private BigDecimal zn;
@TableField("F")
@Schema(description = "氟化物")
private BigDecimal f;
@TableField("SE")
@Schema(description = "")
private BigDecimal se;
@TableField("ARS")
@Schema(description = "")
private BigDecimal ars;
@TableField("HG")
@Schema(description = "")
private BigDecimal hg;
@TableField("CD")
@Schema(description = "")
private BigDecimal cd;
@TableField("CR6")
@Schema(description = "铬(六价)")
private BigDecimal cr6;
@TableField("PB")
@Schema(description = "")
private BigDecimal pb;
@TableField("CN")
@Schema(description = "氰化物")
private BigDecimal cn;
@TableField("VLPH")
@Schema(description = "挥发酚")
private BigDecimal vlph;
@TableField("OIL")
@Schema(description = "石油类")
private BigDecimal oil;
@TableField("LAS")
@Schema(description = "阴离子表面活性剂")
private BigDecimal las;
@TableField("S2")
@Schema(description = "硫化物")
private BigDecimal s2;
@TableField("FCG")
@Schema(description = "粪大肠菌群")
private BigDecimal fcg;
@TableField("CL")
@Schema(description = "氯化物")
private BigDecimal cl;
@TableField("SO4")
@Schema(description = "硫酸盐")
private BigDecimal so4;
@TableField("NO3")
@Schema(description = "硝氮/硝酸盐氮")
private BigDecimal no3;
@TableField("THRD")
@Schema(description = "总硬度")
private BigDecimal thrd;
@TableField("COND")
@Schema(description = "电导率")
private BigDecimal cond;
@TableField("FE")
@Schema(description = "")
private BigDecimal fe;
@TableField("MN")
@Schema(description = "")
private BigDecimal mn;
@TableField("AL")
@Schema(description = "")
private BigDecimal al;
@TableField("CHLA")
@Schema(description = "叶绿素a")
private BigDecimal chla;
@TableField("CLARITY")
@Schema(description = "透明度")
private BigDecimal clarity;
@TableField("TU")
@Schema(description = "浊度")
private BigDecimal tu;
@TableField("CYANO")
@Schema(description = "蓝绿藻")
private BigDecimal cyano;
@TableField("NI")
@Schema(description = "")
private BigDecimal ni;
@TableField("TOD")
@Schema(description = "总氧/总需氧量")
private BigDecimal tod;
@TableField("SFDB")
@Schema(description = "是否达标")
private Integer sfdb;
@TableField("FID")
@Schema(description = "文件ID")
private String fid;
@TableField("VLSR")
@Schema(description = "数据来源")
private String vlsr;
@TableField("WQ_SFDBHN_YS")
@Schema(description = "水质不达标的要素")
private String wqSfdbhnYs;
@TableField("REMARK")
@Schema(description = "备注")
private String remark;
@TableField("TLI_SUM")
@Schema(description = "综合营养状态指数")
private BigDecimal tliSum;
@TableField("TLI_SUM_LEVEL")
@Schema(description = "综合营养状态指数分级")
private String tliSumLevel;
@TableField("CA")
@Schema(description = "")
private BigDecimal ca;
@TableField("PO4")
@Schema(description = "磷酸盐")
private BigDecimal po4;
@TableField("N_NO2")
@Schema(description = "亚硝酸盐氮(弃用)")
private BigDecimal nNo2;
@TableField("NA")
@Schema(description = "")
private BigDecimal na;
@TableField("K")
@Schema(description = "")
private BigDecimal k;
@TableField("TOC")
@Schema(description = "总有机碳")
private BigDecimal toc;
@TableField("OPOR")
@Schema(description = "有机磷农药")
private BigDecimal opor;
@TableField("SS")
@Schema(description = "悬浮物")
private BigDecimal ss;
@TableField("NO2")
@Schema(description = "亚硝酸盐氮")
private BigDecimal no2;
@TableField("ORP")
@Schema(description = "氧化还原电位")
private BigDecimal orp;
@TableField("FWGHTEMP")
@Schema(description = "FWGHTEMP")
private BigDecimal fwghtemp;
@TableField("RECORD_USER")
@Schema(description = "创建人")
private String recordUser;
@TableField("RECORD_TIME")
@Schema(description = "创建时间")
private Date recordTime;
@TableField("MODIFY_USER")
@Schema(description = "更新人")
private String modifyUser;
@TableField("MODIFY_TIME")
@Schema(description = "更新时间")
private Date modifyTime;
@TableField("IS_DELETED")
@Schema(description = "是否已删除")
private Integer isDeleted;
@TableField("DELETE_USER")
@Schema(description = "删除人")
private String deleteUser;
@TableField("DELETE_TIME")
@Schema(description = "删除时间")
private Date deleteTime;
@TableField(exist = false)
private Integer sfdbNew;
@TableField(exist = false)
private String tmNew;
@TableField(exist = false)
private String baseId;
@TableField(exist = false)
private Integer dtinType;
}

View File

@ -0,0 +1,139 @@
package com.yfd.platform.qgc_env.wq.entity.ao;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
@Data
@Schema(description = "定时任务基础参数")
public class JobBaseAo {
public static final String JOB_TYPE_DAY = "day";
public static final String JOB_TYPE_HOUR = "hour";
@Schema(description = "job类型 day=天 hour=小时", example = "hour", allowableValues = "day,hour")
private String jobType;
@Schema(description = "只能是数字或特定字符串,为空=1。job类型为天即单位为天类型为小时即单位小时 today=当天 am15=跨月15天取开始月份1号", example = "2")
private String jobStep;
@Schema(description = "开始时间", example = "2022-08-01 00:00:00")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@Schema(description = "结束时间", example = "2022-08-31 23:59:59")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
@Schema(description = "设施类型")
private String sttpCode;
@Schema(description = "测站编码集合")
private Set<String> stcds;
@Schema(description = "备注")
private String remarks;
public void initTime() {
Date startTime = this.getStartTime();
Date endTime = this.getEndTime();
if (startTime == null || endTime == null) {
Date now = new Date();
String num = this.getJobStep() != null && !"".equals(this.getJobStep().trim()) ? this.getJobStep() : "1";
if ("hour".equals(this.getJobType())) {
if ("today".equalsIgnoreCase(num)) {
startTime = getDayStartTime(now);
endTime = parseWholeMinuteEnd(now);
} else {
startTime = parseWholeMinuteStart(DateUtils.addHours(now, -Integer.parseInt(num)));
endTime = parseWholeMinuteEnd(now);
}
} else if ("day".equals(this.getJobType())) {
if ("today".equalsIgnoreCase(num)) {
startTime = getDayStartTime(now);
endTime = parseWholeMinuteEnd(now);
} else if (num.toLowerCase().startsWith("am")) {
num = num.replace("am", "");
startTime = getMonthFirstDayStartTime(DateUtils.addDays(now, -Integer.parseInt(num)));
endTime = parseWholeMinuteEnd(now);
} else {
startTime = getDayStartTime(DateUtils.addDays(now, -Integer.parseInt(num)));
endTime = parseWholeMinuteEnd(now);
}
}
}
if (startTime != null && endTime != null) {
if (startTime.getTime() > endTime.getTime()) {
throw new RuntimeException("开始时间不能大于结束时间");
} else {
this.startTime = startTime;
this.endTime = endTime;
}
} else {
throw new RuntimeException("开始时间和结束时间不能为空");
}
}
private Date getMonthFirstDayStartTime(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
private static Date getDayStartTime(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
private static Date parseWholeMinuteEnd(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
calendar.set(Calendar.MILLISECOND, 999);
return calendar.getTime();
}
private static Date parseWholeMinuteStart(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
private static class DateUtils {
public static Date addHours(Date date, int hours) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR_OF_DAY, hours);
return calendar.getTime();
}
public static Date addDays(Date date, int days) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, days);
return calendar.getTime();
}
}
}

View File

@ -1,5 +1,6 @@
package com.yfd.platform.qgc_env.wq.entity.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -11,6 +12,8 @@ import java.util.Map;
@Data
@Schema(description = "水质实时数据")
public class EnvWqDataVo {
@Schema(description = "id")
private String id;
@Schema(description = "测站编码")
private String stcd;
@Schema(description = "电站编码")
@ -137,6 +140,34 @@ public class EnvWqDataVo {
private Integer rstcdStepSort;
@Schema(description = "测站排序")
private Integer siteStepSort;
@Schema(description = "创建人")
private String recordUser;
@Schema(description = "创建时间")
private Date recordTime;
@Schema(description = "更新人")
private String modifyUser;
@Schema(description = "更新时间")
private Date modifyTime;
@Schema(description = "是否已删除")
private Integer isDeleted;
@Schema(description = "删除人")
private String deleteUser;
@Schema(description = "删除时间")
private Date deleteTime;
@Schema(description = "水质要素最小限值集合")
private List<Map<String, Object>> min;
@Schema(description = "水质要素最大限值集合")

View File

@ -0,0 +1,18 @@
package com.yfd.platform.qgc_env.wq.mapper;
import com.yfd.platform.qgc_env.wq.entity.SdWqR;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface WqJobMapper {
List<SdWqR> getWqLastData(@Param("ao") JobBaseAo ao);
List<SdWqR> getWqYsLastData(@Param("startTimeStr") String startTimeStr, @Param("endTimeStr") String endTimeStr);
void mergeWqR(@Param("wqRs") List<SdWqR> wqRs);
}

View File

@ -3,6 +3,7 @@ package com.yfd.platform.qgc_env.wq.service;
import com.yfd.platform.common.DataSourceRequest;
import com.yfd.platform.common.DataSourceResult;
import com.yfd.platform.qgc_base.entity.vo.BatchDeleteAo;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
import com.yfd.platform.qgc_env.wq.entity.vo.WqBaseInfoVo;
import com.yfd.platform.qgc_env.wq.entity.vo.EnvWqAnchorPointVo;
@ -45,4 +46,6 @@ public interface EnvWqDataService {
boolean removeKendoByIds(BatchDeleteAo batchDeleteAo);
DataSourceResult<EnvWqAnchorPointVo> getAnchorPointKendoListCust(DataSourceRequest dataSourceRequest);
void wqLastData(JobBaseAo ao);
}

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yfd.platform.common.*;
import com.yfd.platform.common.exception.BizException;
@ -15,15 +16,16 @@ import com.yfd.platform.qgc_base.entity.vo.DataParam;
import com.yfd.platform.qgc_base.mapper.MsOperationLogDetailMapper;
import com.yfd.platform.qgc_base.mapper.MsOperationLogMapper;
import com.yfd.platform.qgc_base.service.IDataScopeFilterService;
import com.yfd.platform.qgc_env.wq.entity.SdWqR;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
import com.yfd.platform.qgc_env.wq.entity.vo.*;
import com.yfd.platform.qgc_env.wq.mapper.WqJobMapper;
import com.yfd.platform.qgc_env.wq.service.EnvWqDataService;
import com.yfd.platform.qgc_env.wt.entity.vo.WbsbVo;
import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils;
import com.yfd.platform.utils.KendoUtil;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import com.yfd.platform.utils.RequestHolder;
import com.yfd.platform.utils.SecurityUtils;
import com.yfd.platform.utils.*;
import jakarta.annotation.Resource;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.jdbc.core.JdbcTemplate;
@ -50,6 +52,9 @@ public class EnvWqDataServiceImpl implements EnvWqDataService {
@Resource
private IDataScopeFilterService dataScopeFilterService;
@Resource
private RedisCacheUtil redisCacheUtil;
private static final String WQ_HOUR_TABLE_NAME = "SD_WQ_R";
private static final String WQ_DAY_TABLE_NAME = "SD_WQDAY_S";
private static final String WQ_MONTH_TABLE_NAME = "SD_WQDRTP_S";
@ -158,6 +163,9 @@ public class EnvWqDataServiceImpl implements EnvWqDataService {
@Resource
private MsOperationLogDetailMapper msOperationLogDetailMapper;
@Resource
private WqJobMapper wqJobMapper;
@Override
public DataSourceResult processKendoList(DataSourceRequest dataSourceRequest) {
boolean calculated = CommonConstant.CALCULATE_SUCCESS.equals(
@ -4549,6 +4557,7 @@ public class EnvWqDataServiceImpl implements EnvWqDataService {
}
@Override
// @Cacheable(cacheNames = "weCache#3600", keyGenerator = "cacheKeyGenerator")
public DataSourceResult<EnvWqAnchorPointVo> getAnchorPointKendoListCust(DataSourceRequest dataSourceRequest) {
DataSourceResult<EnvWqAnchorPointVo> dataSourceResult = new DataSourceResult<>();
dataSourceResult.setAggregates(new HashMap<>());
@ -4575,6 +4584,15 @@ public class EnvWqDataServiceImpl implements EnvWqDataService {
.append("t.ELEV AS dtmel, ")
.append("t.STLC AS stlc, ")
.append("t.WWQTG AS wwqtg, ")
.append("CASE t.WWQTG " +
" WHEN '1' THEN '' " +
" WHEN '2' THEN 'Ⅱ' " +
" WHEN '3' THEN 'Ⅲ' " +
" WHEN '4' THEN 'Ⅳ' " +
" WHEN '5' THEN '' " +
" WHEN '6' THEN 'V' " +
" ELSE '未知' " +
"END AS wwqtgName,")
.append("t.DTIN_TYPE AS dtinType, ")
.append("t.RSTCD AS rstcd, ")
.append("t.FHSTCD AS fhstcd, ")
@ -4640,15 +4658,16 @@ public class EnvWqDataServiceImpl implements EnvWqDataService {
.distinct()
.collect(Collectors.toList());
List<EnvWqDataVo> wqDataList = queryWqDataByStcdBatch(stcdList);
Map<String, EnvWqDataVo> wqDataMap = wqDataList.stream()
.collect(Collectors.toMap(EnvWqDataVo::getStcd, Function.identity(), (oldValue, newValue) -> newValue));
// List<EnvWqDataVo> wqDataList = queryWqDataByStcdBatch(stcdList);
// Map<String, EnvWqDataVo> wqDataMap = wqDataList.stream()
// .collect(Collectors.toMap(EnvWqDataVo::getStcd, Function.identity(), (oldValue, newValue) -> newValue));
for (EnvWqAnchorPointVo anchorPointVo : wqVoList) {
EnvWqDataVo wqData = wqDataMap.get(anchorPointVo.getStcd());
EnvWqDataVo wqData = redisCacheUtil.fromJson(redisCacheUtil.getString("wqLastDate:" + anchorPointVo.getStcd()), EnvWqDataVo.class);
if (wqData != null) {
anchorPointVo.setTm(wqData.getTm());
anchorPointVo.setWqGrd(wqData.getWqgrd());
anchorPointVo.setWqGrdName(wqData.getWqgrdName());
anchorPointVo.setSfdb(wqData.getSfdb());
anchorPointVo.setWtmp(wqData.getWtmp());
anchorPointVo.setPh(wqData.getPh());
@ -4730,7 +4749,7 @@ public class EnvWqDataServiceImpl implements EnvWqDataService {
.map(s -> "'" + s + "'")
.collect(Collectors.joining(","));
String wqDataSql = "SELECT STCD, TM, WQGRD, SFDB, WTMP, PH, DOX, CODMN, CODCR, BOD5, NH3N, TP, TN, CU, ZN, F, SE, ARS, HG, CD, CR6, PB, CN, VLPH, OIL, LAS, S2, FCG, CL, SO4, NO3, THRD, COND, FE, MN, AL, CHLA, CLARITY, TU, CYANO, TOD " +
String wqDataSql = "SELECT STCD, TM, WQGRD,CASE WQGRD WHEN '1' THEN '' WHEN '2' THEN 'Ⅱ' WHEN '3' THEN 'Ⅲ' WHEN '4' THEN 'Ⅳ' WHEN '5' THEN '' WHEN '6' THEN 'V' ELSE '未知' END AS wqgrdName, SFDB, WTMP, PH, DOX, CODMN, CODCR, BOD5, NH3N, TP, TN, CU, ZN, F, SE, ARS, HG, CD, CR6, PB, CN, VLPH, OIL, LAS, S2, FCG, CL, SO4, NO3, THRD, COND, FE, MN, AL, CHLA, CLARITY, TU, CYANO, TOD " +
"FROM ( " +
" SELECT t.*, ROW_NUMBER() OVER (PARTITION BY t.STCD ORDER BY t.TM DESC) AS rn " +
" FROM SD_WQ_R t " +
@ -4952,4 +4971,29 @@ public class EnvWqDataServiceImpl implements EnvWqDataService {
}
}
}
@Override
public void wqLastData(JobBaseAo ao) {
List<SdWqR> wqDataVos = wqJobMapper.getWqLastData(ao);
String startTimeStr = DateUtil.format(ao.getStartTime(), "yyyy-MM-dd HH:mm:ss");
String endTimeStr = DateUtil.format(ao.getEndTime(), "yyyy-MM-dd HH:mm:ss");
List<SdWqR> wqYsDataVos = wqJobMapper.getWqYsLastData(startTimeStr, endTimeStr);
if (CollectionUtils.isNotEmpty(wqYsDataVos)) {
for (SdWqR wqDataVo : wqYsDataVos) {
redisCacheUtil.setString("wqYsLastDate:" + wqDataVo.getStcd(), JSONUtil.toJsonStr(wqDataVo));
}
}
if (CollUtil.isEmpty(wqDataVos)) {
return;
}
if (!wqDataVos.isEmpty()) {
List<List<SdWqR>> newWqrListPartition = CollUtil.split(wqDataVos, 300);
for (List<SdWqR> t : newWqrListPartition) {
wqJobMapper.mergeWqR(t);
}
}
for (SdWqR wqDataVo : wqDataVos) {
redisCacheUtil.setString("wqLastDate:" + wqDataVo.getStcd(), JSONUtil.toJsonStr(wqDataVo));
}
}
}

View File

@ -27,7 +27,10 @@ import com.yfd.platform.qgc_env.wt.entity.vo.WaterTempMapVO;
import com.yfd.platform.qgc_env.wt.mapper.SdWtMonitorMapper;
import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils;
import com.yfd.platform.qgc_env.wt.service.SdWtMonitorService;
import com.yfd.platform.qgc_job.vo.WtLastDataVo;
import com.yfd.platform.qgc_job.vo.WtvtPointVo;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import com.yfd.platform.utils.RedisCacheUtil;
import com.yfd.platform.utils.SecurityUtils;
import jakarta.annotation.Resource;
import lombok.Data;
@ -63,6 +66,9 @@ public class SdWtMonitorServiceImpl implements SdWtMonitorService {
@Resource
private IDataScopeFilterService dataScopeFilterService;
@Resource
private RedisCacheUtil redisCacheUtil;
@Override
public DataSourceResult getEvnmAutoMonitorList(DataSourceRequest dataSourceRequest) {
DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest();
@ -3044,6 +3050,19 @@ public class SdWtMonitorServiceImpl implements SdWtMonitorService {
vo.setAnchoPointState("WT_2");
}
}
if(vo.getEnfc()==1){
//垂向水温
WtvtPointVo wtvtPointVo =redisCacheUtil.fromJson(redisCacheUtil.getString("wtvtPointData:" + vo.getStcd()), WtvtPointVo.class);
vo.setWtvtInfo(wtvtPointVo);
}else{
//河道水温
WtLastDataVo sdWtrvR = redisCacheUtil.fromJson(redisCacheUtil.getString("wtLastData:" + vo.getStcd()), WtLastDataVo.class);
if(sdWtrvR !=null){
vo.setTm(sdWtrvR.getTm());
vo.setTemperature(sdWtrvR.getWt());
}
}
}
SiteAvoidanceUtils.calcSiteMapLev(resultList,

View File

@ -16,6 +16,7 @@ import com.yfd.platform.qgc_env.wte.service.WeFishService;
import com.yfd.platform.utils.KendoUtil;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import jakarta.annotation.Resource;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@ -1035,6 +1036,7 @@ public class WeFishServiceImpl implements WeFishService {
}
@Override
@Cacheable(cacheNames = "weCache#3600", keyGenerator = "cacheKeyGenerator")
public DataSourceResult<WeFishPointQgcVo> getQgcWeFishPoint(DataSourceRequest dataSourceRequest) {
DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest();
String stcd = resolveWeFishStringFilterValue(dataSourceRequest, loadOptions, "stcd");
@ -1067,6 +1069,7 @@ public class WeFishServiceImpl implements WeFishService {
}
@Override
@Cacheable(cacheNames = "weCache#3600", keyGenerator = "cacheKeyGenerator")
public DataSourceResult<WePointQgcVo> getQgcWePoint(DataSourceRequest dataSourceRequest) {
DataSourceResult<WePointQgcVo> result = new DataSourceResult<>();
result.setAggregates(new HashMap<>());

View File

@ -24,7 +24,9 @@ import com.yfd.platform.qgc_env.zq.entity.vo.ZqRiverDrtpDataVo;
import com.yfd.platform.qgc_env.zq.entity.vo.ZqRiverDataVo;
import com.yfd.platform.qgc_env.wt.utils.SiteAvoidanceUtils;
import com.yfd.platform.qgc_env.zq.service.ZqMonitorService;
import com.yfd.platform.qgc_job.vo.ZqLastDataVo;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import com.yfd.platform.utils.RedisCacheUtil;
import com.yfd.platform.utils.SecurityUtils;
import jakarta.annotation.Resource;
import org.springframework.cache.annotation.Cacheable;
@ -50,7 +52,6 @@ public class ZqMonitorServiceImpl implements ZqMonitorService {
private static final String ZQ_MONTH_TABLE_NAME = "SD_RIVERDRTP_S";
private static final Map<String, String> ZQ_UPDATE_COLUMN_MAP = new LinkedHashMap<>();
private static final Map<String, String> ZQ_FIELD_MEANING_MAP = new LinkedHashMap<>();
static {
ZQ_UPDATE_COLUMN_MAP.put("z", "Z");
ZQ_UPDATE_COLUMN_MAP.put("q", "Q");
@ -90,6 +91,8 @@ public class ZqMonitorServiceImpl implements ZqMonitorService {
@Resource
private MsOperationLogDetailMapper msOperationLogDetailMapper;
@Resource
private RedisCacheUtil redisCacheUtil;
@Override
@Cacheable(cacheNames = "zqCache#7200", keyGenerator = "cacheKeyGenerator")
@ -2374,6 +2377,7 @@ public class ZqMonitorServiceImpl implements ZqMonitorService {
}
@Override
@Cacheable(cacheNames = "zqCache#3600", keyGenerator = "cacheKeyGenerator")
public DataSourceResult<ZqPoint> getZqPointKendoListCust(DataSourceRequest dataSourceRequest) {
DataSourceResult<ZqPoint> dataSourceResult = new DataSourceResult<>();
dataSourceResult.setAggregates(new HashMap<>());
@ -2440,7 +2444,16 @@ public class ZqMonitorServiceImpl implements ZqMonitorService {
Page<?> page = QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake());
List<ZqPoint> resultList = microservicDynamicSQLMapper.pageAllListWithResultType(page, sql.toString(), paramMap, ZqPoint.class);
resultList.forEach(item->{
//获取流量站最新数据
ZqLastDataVo zqLastData = redisCacheUtil.fromJson(redisCacheUtil.getString("zqLastData:"+item.getStcd()), ZqLastDataVo.class);
if(zqLastData !=null){
item.setTm(zqLastData.getTm());
item.setQ(zqLastData.getQ());
item.setZ(zqLastData.getZ());
item.setV(zqLastData.getV());
}
});
SiteAvoidanceUtils.calcSiteMapLev(resultList,
ZqPoint::getStcd,
ZqPoint::getLgtd,

View File

@ -0,0 +1,10 @@
package com.yfd.platform.qgc_job.service;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
public interface IDwJobService {
void wtLastData(JobBaseAo ao);
void wtvtPointData(JobBaseAo ao);
}

View File

@ -0,0 +1,8 @@
package com.yfd.platform.qgc_job.service;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
public interface IEqJobService {
void eqAnchorPointLastDate(JobBaseAo ao);
}

View File

@ -0,0 +1,8 @@
package com.yfd.platform.qgc_job.service;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
public interface IFhJobService {
void zqLastData(JobBaseAo ao);
}

View File

@ -0,0 +1,291 @@
package com.yfd.platform.qgc_job.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.yfd.platform.common.MicroservicDynamicSQLMapper;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
import com.yfd.platform.qgc_job.service.IDwJobService;
import com.yfd.platform.qgc_job.vo.WtLastDataVo;
import com.yfd.platform.qgc_job.vo.WtvtPointDataVo;
import com.yfd.platform.qgc_job.vo.WtvtPointVo;
import com.yfd.platform.utils.RedisCacheUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Slf4j
@Service
public class DwJobServiceImpl implements IDwJobService {
private static final String WT_LAST_DATA_KEY_PREFIX = "wtLastData:";
private static final String WTVT_POINT_DATA_KEY_PREFIX = "wtvtPointData:";
@Resource
private MicroservicDynamicSQLMapper<?> microservicDynamicSQLMapper;
@Resource
private RedisCacheUtil redisCacheUtil;
@Override
public void wtLastData(JobBaseAo ao) {
if (ao == null) {
throw new RuntimeException("任务参数不能为空");
}
ao.initTime();
Date startTime = ao.getStartTime();
Date endTime = ao.getEndTime();
Set<String> stcds = ao.getStcds();
List<Map<String, Date>> dateList = getInterValDate(startTime, endTime);
Map<String, WtLastDataVo> lastDataMap = new LinkedHashMap<>();
for (int index = dateList.size() - 1; index >= 0; index--) {
Map<String, Date> dateMap = dateList.get(index);
Date segmentStartTime = dateMap.get("startTime");
Date segmentEndTime = dateMap.get("endTime");
List<WtLastDataVo> wtLastDataList = getWtLastData(stcds, segmentStartTime, segmentEndTime);
for (WtLastDataVo wtLastDataVo : wtLastDataList) {
if (wtLastDataVo == null || wtLastDataVo.getStcd() == null || lastDataMap.containsKey(wtLastDataVo.getStcd())) {
continue;
}
lastDataMap.put(wtLastDataVo.getStcd(), wtLastDataVo);
}
}
int count = 0;
for (Map.Entry<String, WtLastDataVo> entry : lastDataMap.entrySet()) {
String stcd = entry.getKey();
WtLastDataVo wtLastDataVo = entry.getValue();
WtLastDataVo redisData = redisCacheUtil.fromJson(redisCacheUtil.getString(WT_LAST_DATA_KEY_PREFIX + stcd), WtLastDataVo.class);
if (shouldRefresh(redisData, wtLastDataVo)) {
redisCacheUtil.setString(WT_LAST_DATA_KEY_PREFIX + stcd, JSONUtil.toJsonStr(wtLastDataVo));
count++;
} else {
redisCacheUtil.delete(WT_LAST_DATA_KEY_PREFIX + stcd);
}
}
log.info("本次更新水温缓存{}条", count);
}
@Override
public void wtvtPointData(JobBaseAo ao) {
if (ao == null) {
throw new RuntimeException("任务参数不能为空");
}
ao.initTime();
Date startTime = ao.getStartTime();
Date endTime = ao.getEndTime();
Set<String> stcds = ao.getStcds();
List<Map<String, Date>> dateList = getInterValDate(startTime, endTime);
Map<String, List<WtvtPointDataVo>> lastDataMap = new LinkedHashMap<>();
for (int index = dateList.size() - 1; index >= 0; index--) {
Map<String, Date> dateMap = dateList.get(index);
Date segmentStartTime = dateMap.get("startTime");
Date segmentEndTime = dateMap.get("endTime");
List<WtvtPointDataVo> wtvtLastDataList = getWtvtPointLastData(stcds, segmentStartTime, segmentEndTime);
if (CollUtil.isEmpty(wtvtLastDataList)) {
continue;
}
Map<String, List<WtvtPointDataVo>> groupedMap = new LinkedHashMap<>();
for (WtvtPointDataVo wtvtPointDataVo : wtvtLastDataList) {
if (wtvtPointDataVo == null || wtvtPointDataVo.getStcd() == null) {
continue;
}
groupedMap.computeIfAbsent(wtvtPointDataVo.getStcd(), key -> new ArrayList<>()).add(wtvtPointDataVo);
}
for (Map.Entry<String, List<WtvtPointDataVo>> entry : groupedMap.entrySet()) {
if (!lastDataMap.containsKey(entry.getKey())) {
lastDataMap.put(entry.getKey(), entry.getValue());
}
}
}
int count = 0;
for (Map.Entry<String, List<WtvtPointDataVo>> entry : lastDataMap.entrySet()) {
String stcd = entry.getKey();
WtvtPointVo wtvtPointVo = buildWtvtPointVo(stcd, entry.getValue());
WtvtPointVo redisData = redisCacheUtil.fromJson(redisCacheUtil.getString(WTVT_POINT_DATA_KEY_PREFIX + stcd), WtvtPointVo.class);
if (shouldRefresh(redisData, wtvtPointVo)) {
redisCacheUtil.setString(WTVT_POINT_DATA_KEY_PREFIX + wtvtPointVo.getStcd(), JSONUtil.toJsonStr(wtvtPointVo));
count++;
}
}
log.info("本次更新垂向水温缓存{}条", count);
}
private boolean shouldRefresh(WtLastDataVo redisData, WtLastDataVo currentData) {
if (currentData == null || currentData.getTm() == null) {
return false;
}
return redisData == null || redisData.getTm() == null || DateUtil.compare(redisData.getTm(), currentData.getTm()) < 0;
}
private boolean shouldRefresh(WtvtPointVo redisData, WtvtPointVo currentData) {
if (currentData == null || currentData.getTm() == null) {
return false;
}
return redisData == null || redisData.getTm() == null || DateUtil.compare(redisData.getTm(), currentData.getTm()) < 0;
}
private List<WtLastDataVo> getWtLastData(Set<String> stcds, Date startTime, Date endTime) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("startTime", startTime);
paramMap.put("endTime", endTime);
StringBuilder sql = new StringBuilder();
sql.append("SELECT ")
.append(" data.ID AS id, ")
.append(" data.STCD AS stcd, ")
.append(" data.TM AS tm, ")
.append(" data.WT AS wt, ")
.append(" data.REMARK AS remark, ")
.append(" data.FID AS fid, ")
.append(" data.RECORD_USER AS recordUser, ")
.append(" data.RECORD_TIME AS recordTime, ")
.append(" data.MODIFY_USER AS modifyUser, ")
.append(" data.MODIFY_TIME AS modifyTime, ")
.append(" data.IS_DELETED AS isDeleted, ")
.append(" data.DELETE_USER AS deleteUser, ")
.append(" data.DELETE_TIME AS deleteTime ")
.append("FROM ( ")
.append(" SELECT t.*, ROW_NUMBER() OVER (PARTITION BY t.STCD ORDER BY t.TM DESC, t.RECORD_TIME DESC, t.ID DESC) AS rn ")
.append(" FROM SD_WTRV_R t ")
.append(" WHERE NVL(t.IS_DELETED, 0) = 0 ")
.append(" AND t.TM >= #{map.startTime} ")
.append(" AND t.TM <= #{map.endTime} ");
appendInCondition(sql, paramMap, "t.STCD", stcds, "wtLastDataStcd");
sql.append(") data WHERE data.rn = 1");
return microservicDynamicSQLMapper.getAllListWithResultType(sql.toString(), paramMap, WtLastDataVo.class);
}
private List<WtvtPointDataVo> getWtvtPointLastData(Set<String> stcds, Date startTime, Date endTime) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("startTime", startTime);
paramMap.put("endTime", endTime);
StringBuilder sql = new StringBuilder();
sql.append("WITH temp AS ( ")
.append(" SELECT STCD, VWT, TM, WTHG ")
.append(" FROM ( ")
.append(" SELECT STCD, VWT, TM, WTHG, DENSE_RANK() OVER (PARTITION BY STCD ORDER BY TM DESC) AS rn ")
.append(" FROM SD_WTVT_R ")
.append(" WHERE NVL(IS_DELETED, 0) = 0 ")
.append(" AND TM >= #{map.startTime} ")
.append(" AND TM <= #{map.endTime} ");
appendInCondition(sql, paramMap, "STCD", stcds, "wtvtPointDataStcd");
sql.append(" ) ")
.append(" WHERE rn = 1 ")
.append(") ")
.append("SELECT ")
.append(" t1.STCD AS stcd, ")
.append(" t2.TM AS tm, ")
.append(" t1.AVGWT AS avgwt, ")
.append(" t2.VWT AS vwt, ")
.append(" t2.WTHG AS wthg, ")
.append(" t2.MINRN AS minrn, ")
.append(" t2.MAXRN AS maxrn ")
.append("FROM ( ")
.append(" SELECT STCD, ROUND(AVG(VWT), 2) AS AVGWT ")
.append(" FROM temp GROUP BY STCD ")
.append(") t1 ")
.append("INNER JOIN ( ")
.append(" SELECT * FROM ( ")
.append(" SELECT ")
.append(" STCD, VWT, TM, WTHG, ")
.append(" ROW_NUMBER() OVER (PARTITION BY STCD ORDER BY WTHG ASC) AS minrn, ")
.append(" ROW_NUMBER() OVER (PARTITION BY STCD ORDER BY WTHG DESC) AS maxrn ")
.append(" FROM temp ")
.append(" ) WHERE MINRN = 1 OR MAXRN = 1 ")
.append(") t2 ON t1.STCD = t2.STCD");
return microservicDynamicSQLMapper.getAllListWithResultType(sql.toString(), paramMap, WtvtPointDataVo.class);
}
private WtvtPointVo buildWtvtPointVo(String stcd, List<WtvtPointDataVo> wtvtPointDataVoList) {
WtvtPointVo wtvtPointVo = new WtvtPointVo();
wtvtPointVo.setStcd(stcd);
if (CollUtil.isEmpty(wtvtPointDataVoList)) {
return wtvtPointVo;
}
for (WtvtPointDataVo wtvtPointDataVo : wtvtPointDataVoList) {
if (wtvtPointDataVo == null) {
continue;
}
wtvtPointVo.setTm(wtvtPointDataVo.getTm());
wtvtPointVo.setAvgWt(wtvtPointDataVo.getAvgwt());
if (Integer.valueOf(1).equals(wtvtPointDataVo.getMinrn())) {
wtvtPointVo.setMinWthg(wtvtPointDataVo.getWthg());
wtvtPointVo.setMinWthgWt(wtvtPointDataVo.getVwt());
}
if (Integer.valueOf(1).equals(wtvtPointDataVo.getMaxrn())) {
wtvtPointVo.setMaxWthg(wtvtPointDataVo.getWthg());
wtvtPointVo.setMaxWthgWt(wtvtPointDataVo.getVwt());
}
}
return wtvtPointVo;
}
// 以指定时间间隔分隔开始时间和结束时间保持旧平台 7 天分段口径
private List<Map<String, Date>> getInterValDate(Date startTime, Date endTime) {
int num = 7;
List<Map<String, Date>> dateList = new ArrayList<>();
long interval = DateUtil.betweenDay(startTime, endTime, false);
if (interval <= num) {
Map<String, Date> dateMap = new HashMap<>();
dateMap.put("startTime", startTime);
dateMap.put("endTime", endTime);
dateList.add(dateMap);
return dateList;
}
while (startTime.getTime() < endTime.getTime()) {
Map<String, Date> dateMap = new HashMap<>();
DateTime dateTime = DateUtil.offsetDay(startTime, num);
dateMap.put("startTime", startTime);
if (dateTime.getTime() < endTime.getTime()) {
dateMap.put("endTime", DateUtil.offsetSecond(dateTime, -1));
} else {
dateMap.put("endTime", endTime);
}
dateList.add(dateMap);
startTime = dateTime;
}
dateList.sort(Comparator.comparing(item -> item.get("startTime")));
return dateList;
}
private void appendInCondition(StringBuilder sql,
Map<String, Object> paramMap,
String column,
Set<String> values,
String prefix) {
if (CollUtil.isEmpty(values)) {
return;
}
List<String> placeholders = new ArrayList<>();
int index = 0;
for (String value : values) {
if (value == null || value.trim().isEmpty()) {
continue;
}
String key = prefix + index++;
paramMap.put(key, value.trim());
placeholders.add("#{map." + key + "}");
}
if (CollUtil.isEmpty(placeholders)) {
return;
}
sql.append(" AND ").append(column).append(" IN (")
.append(String.join(", ", placeholders))
.append(") ");
}
}

View File

@ -0,0 +1,85 @@
package com.yfd.platform.qgc_job.service.impl;
import cn.hutool.json.JSONUtil;
import com.yfd.platform.common.MicroservicDynamicSQLMapper;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
import com.yfd.platform.qgc_job.service.IEqJobService;
import com.yfd.platform.qgc_job.vo.EqAnchorPointLastDateVo;
import com.yfd.platform.utils.RedisCacheUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class EqJobServiceImpl implements IEqJobService {
@Resource
private MicroservicDynamicSQLMapper<?> microservicDynamicSQLMapper;
@Resource
private RedisCacheUtil redisCacheUtil;
@Override
public void eqAnchorPointLastDate(JobBaseAo ao) {
Map<String, Object> paramMap = new HashMap<>();
// 旧逻辑只按当前时间截取最新一条工程运行数据ao 在此方法中不参与过滤
paramMap.put("currentTime", new Date());
List<EqAnchorPointLastDateVo> eqDataVos = microservicDynamicSQLMapper.getAllListWithResultType(
buildEqAnchorPointLastDateSql(),
paramMap,
EqAnchorPointLastDateVo.class
);
for (EqAnchorPointLastDateVo eqDataVo : eqDataVos) {
redisCacheUtil.setString("eqAnchorPointLastDate:" + eqDataVo.getStcd(), JSONUtil.toJsonStr(eqDataVo));
}
}
private String buildEqAnchorPointLastDateSql() {
return "SELECT " +
"run.STCD AS stcd, " +
"run.TM AS tm, " +
"run.QI AS qi, " +
"run.QO AS qo, " +
"run.RZ AS rz, " +
"run.DZ AS dz, " +
"qec.QEC_LIMIT AS qecLimit, " +
"qec.QEC_C AS qecC, " +
"qec.MWR_LIMIT AS mwrLimit, " +
"qec.MWR_C AS mwrC, " +
"NVL(qec.AVQ_LIMIT, eng.AVQ) AS avqLimit, " +
"qec.AVQ_C AS avqC, " +
"qecDetail.VLSR AS vlsr, " +
"qecDetail.VLSR_TM AS vlsrTm, " +
"mwrDetail.VLSR AS mwrVlsr, " +
"mwrDetail.VLSR_TM AS mwrVlsrTm " +
"FROM ( " +
" SELECT STCD, MAX(TM) AS TM " +
" FROM SD_HYDROPW_R " +
" WHERE NVL(IS_DELETED, 0) = 0 " +
" AND TM <= #{map.currentTime} " +
" GROUP BY STCD " +
") lastRun " +
"INNER JOIN SD_HYDROPW_R run " +
" ON lastRun.STCD = run.STCD " +
" AND lastRun.TM = run.TM " +
" AND NVL(run.IS_DELETED, 0) = 0 " +
"LEFT JOIN SD_QEC_R qec " +
" ON qec.STCD = run.STCD " +
" AND qec.TM = run.TM " +
" AND NVL(qec.IS_DELETED, 0) = 0 " +
"LEFT JOIN SD_ENGINFO_B_H eng " +
" ON eng.STCD = run.STCD " +
" AND NVL(eng.IS_DELETED, 0) = 0 " +
"LEFT JOIN MS_WARN_RULE_DETAIL_B qecDetail " +
" ON qecDetail.ID = qec.QEC_RULE_DID " +
" AND NVL(qecDetail.IS_DELETED, 0) = 0 " +
"LEFT JOIN MS_WARN_RULE_DETAIL_B mwrDetail " +
" ON mwrDetail.ID = qec.QEC_MWRRULE_DID " +
" AND NVL(mwrDetail.IS_DELETED, 0) = 0";
}
}

View File

@ -0,0 +1,95 @@
package com.yfd.platform.qgc_job.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.yfd.platform.common.MicroservicDynamicSQLMapper;
import com.yfd.platform.qgc_env.wq.entity.ao.JobBaseAo;
import com.yfd.platform.qgc_job.service.IFhJobService;
import com.yfd.platform.qgc_job.vo.ZqLastDataVo;
import com.yfd.platform.utils.RedisCacheUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Slf4j
@Service
public class FhJobServiceImpl implements IFhJobService {
private static final String ZQ_LAST_DATA_KEY_PREFIX = "zqLastData:";
@Resource
private MicroservicDynamicSQLMapper<?> microservicDynamicSQLMapper;
@Resource
private RedisCacheUtil redisCacheUtil;
@Override
public void zqLastData(JobBaseAo ao) {
Set<String> stcds = ao == null ? null : ao.getStcds();
List<ZqLastDataVo> zqDataVos = getZqLastData(stcds);
if (CollUtil.isEmpty(zqDataVos)) {
return;
}
for (ZqLastDataVo zqDataVo : zqDataVos) {
if (zqDataVo == null || zqDataVo.getStcd() == null) {
continue;
}
redisCacheUtil.setString(ZQ_LAST_DATA_KEY_PREFIX + zqDataVo.getStcd(), JSONUtil.toJsonStr(zqDataVo));
}
log.info("本次更新河道水情缓存{}条", zqDataVos.size());
}
private List<ZqLastDataVo> getZqLastData(Set<String> stcds) {
Map<String, Object> paramMap = new HashMap<>();
StringBuilder sql = new StringBuilder();
sql.append("SELECT ")
.append("t.STCD AS stcd, ")
.append("t.Z AS z, ")
.append("t.Q AS q, ")
.append("t.V AS v, ")
.append("t.TM AS tm ")
.append("FROM SD_RIVER_R t ")
.append("INNER JOIN ( ")
.append(" SELECT STCD, MAX(TM) AS tm ")
.append(" FROM SD_RIVER_R ")
.append(" WHERE NVL(IS_DELETED, 0) = 0 ");
appendInCondition(sql, paramMap, "STCD", stcds, "zqLastDataInnerStcd");
sql.append(" GROUP BY STCD ")
.append(") t1 ON t.STCD = t1.STCD AND t.TM = t1.tm ")
.append("WHERE NVL(t.IS_DELETED, 0) = 0 ");
appendInCondition(sql, paramMap, "t.STCD", stcds, "zqLastDataOuterStcd");
return microservicDynamicSQLMapper.getAllListWithResultType(sql.toString(), paramMap, ZqLastDataVo.class);
}
private void appendInCondition(StringBuilder sql,
Map<String, Object> paramMap,
String column,
Set<String> values,
String prefix) {
if (CollUtil.isEmpty(values)) {
return;
}
List<String> placeholders = new ArrayList<>();
int index = 0;
for (String value : values) {
if (value == null || value.trim().isEmpty()) {
continue;
}
String key = prefix + index++;
paramMap.put(key, value.trim());
placeholders.add("#{map." + key + "}");
}
if (CollUtil.isEmpty(placeholders)) {
return;
}
sql.append(" AND ").append(column).append(" IN (")
.append(String.join(", ", placeholders))
.append(") ");
}
}

View File

@ -0,0 +1,79 @@
package com.yfd.platform.qgc_job.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Schema(description = "电站锚点最新缓存数据")
public class EqAnchorPointLastDateVo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "电站编码")
private String stcd;
@Schema(description = "电站名称")
private String stnm;
@Schema(description = "数据时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date tm;
@Schema(description = "装机容量")
private BigDecimal ttpwr;
@Schema(description = "生态流量限值(环保部)")
private BigDecimal qecLimit;
@Schema(description = "生态流量限值(水利部)")
private BigDecimal mwrLimit;
@Schema(description = "生态流量限值(多年平均)")
private BigDecimal avqLimit;
@Schema(description = "生态流量系数(环保部)")
private BigDecimal qecC;
@Schema(description = "生态流量系数(水利部)")
private BigDecimal mwrC;
@Schema(description = "生态流量系数(多年平均)")
private BigDecimal avqC;
@Schema(description = "正常蓄水位")
private BigDecimal normz;
@Schema(description = "死水位")
private BigDecimal ddz;
@Schema(description = "入库流量")
private BigDecimal qi;
@Schema(description = "出库流量")
private BigDecimal qo;
@Schema(description = "坝上水位")
private BigDecimal rz;
@Schema(description = "坝下水位")
private BigDecimal dz;
@Schema(description = "规则来源")
private String vlsr;
@Schema(description = "规则来源时间")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date vlsrTm;
@Schema(description = "规则来源(水利部)")
private String mwrVlsr;
@Schema(description = "规则来源时间(水利部)")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date mwrVlsrTm;
}

View File

@ -0,0 +1,59 @@
package com.yfd.platform.qgc_job.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Schema(description = "水温最新缓存数据")
public class WtLastDataVo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
private String id;
@Schema(description = "站点编码")
private String stcd;
@Schema(description = "数据时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date tm;
@Schema(description = "水温")
private BigDecimal wt;
@Schema(description = "备注")
private String remark;
@Schema(description = "附件ID")
private String fid;
@Schema(description = "创建人")
private String recordUser;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date recordTime;
@Schema(description = "更新人")
private String modifyUser;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date modifyTime;
@Schema(description = "是否删除")
private Integer isDeleted;
@Schema(description = "删除人")
private String deleteUser;
@Schema(description = "删除时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date deleteTime;
}

View File

@ -0,0 +1,37 @@
package com.yfd.platform.qgc_job.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@Schema(description = "垂向水温锚点中间数据")
public class WtvtPointDataVo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "站点编码")
private String stcd;
@Schema(description = "数据时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date tm;
@Schema(description = "平均水温")
private Double avgwt;
@Schema(description = "水温")
private Double vwt;
@Schema(description = "测量深度")
private Double wthg;
@Schema(description = "最小深度行号")
private Integer minrn;
@Schema(description = "最大深度行号")
private Integer maxrn;
}

View File

@ -0,0 +1,37 @@
package com.yfd.platform.qgc_job.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@Schema(description = "垂向水温锚点缓存数据")
public class WtvtPointVo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "站点编码")
private String stcd;
@Schema(description = "数据时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date tm;
@Schema(description = "平均温度")
private Double avgWt;
@Schema(description = "最大测量深度")
private Double maxWthg;
@Schema(description = "最大测量深度水温")
private Double maxWthgWt;
@Schema(description = "最小测量深度")
private Double minWthg;
@Schema(description = "最小测量深度水温")
private Double minWthgWt;
}

View File

@ -0,0 +1,27 @@
package com.yfd.platform.qgc_job.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Schema(description = "河道水情最新缓存")
public class ZqLastDataVo {
@Schema(description = "站码")
private String stcd;
@Schema(description = "水位")
private BigDecimal z;
@Schema(description = "流量")
private BigDecimal q;
@Schema(description = "流速")
private BigDecimal v;
@Schema(description = "时间")
private Date tm;
}

View File

@ -1,6 +1,9 @@
package com.yfd.platform.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@ -12,8 +15,9 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class RedisCacheUtil {
static final ObjectMapper objectMapper = new ObjectMapper();
@Resource
private RedisTemplate<String, Object> redisTemplate;
@ -41,6 +45,21 @@ public class RedisCacheUtil {
return (T) value;
}
public <T> T fromJson(String json, Class<T> clazz) {
if (!StringUtils.isBlank(json) && clazz != null) {
try {
return (T)objectMapper.readValue(json, clazz);
} catch (Exception var4) {
if (log.isErrorEnabled()) {
log.error(var4.getMessage(), var4);
}
return null;
}
} else {
return null;
}
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yfd.platform.qgc_env.wq.mapper.WqJobMapper">
<select id="getWqLastData" resultType="com.yfd.platform.qgc_env.wq.entity.SdWqR">
SELECT SHR.*,CASE SHR.WQGRD
WHEN '1' THEN ''
WHEN '2' THEN 'Ⅱ'
WHEN '3' THEN 'Ⅲ'
WHEN '4' THEN 'Ⅳ'
WHEN '5' THEN ''
WHEN '6' THEN 'V'
ELSE '未知'
END AS wqgrdName FROM (
SELECT STCD, MAX(TM) TM FROM SD_WQ_R WHERE NVL(IS_DELETED, 0) = 0 GROUP BY STCD
) T LEFT JOIN SD_WQ_R SHR ON T.STCD = SHR.STCD AND SHR.TM = T.TM
</select>
<select id="getWqYsLastData" resultType="com.yfd.platform.qgc_env.wq.entity.SdWqR">
SELECT * FROM (
SELECT SD_WQ_R.*,CASE SD_WQ_R.WQGRD
WHEN '1' THEN ''
WHEN '2' THEN 'Ⅱ'
WHEN '3' THEN 'Ⅲ'
WHEN '4' THEN 'Ⅳ'
WHEN '5' THEN ''
WHEN '6' THEN 'V'
ELSE '未知'
END AS wqgrdName,
RANK() OVER (PARTITION BY STCD ORDER BY TM DESC) AS RANK
FROM SD_WQ_R
WHERE NVL(IS_DELETED, 0) = 0
AND <![CDATA[ TM >= TO_DATE(#{startTimeStr}, 'yyyy-MM-dd HH24:mi:ss') ]]>
AND <![CDATA[ TM <= TO_DATE(#{endTimeStr}, 'yyyy-MM-dd HH24:mi:ss') ]]>
AND (NH3N IS NOT NULL OR TP IS NOT NULL OR TN IS NOT NULL)
) WHERE RANK = 1
</select>
<update id="mergeWqR">
MERGE INTO SD_WQ_R T
USING (
<foreach collection="wqRs" index="index" item="wqR" open="" close="" separator="union all">
SELECT #{wqR.id,jdbcType=VARCHAR} AS ID
, #{wqR.wqgrd,jdbcType=VARCHAR} AS WQGRD
, #{wqR.sfdb,jdbcType=NUMERIC} AS SFDB
, #{wqR.wqSfdbhnYs,jdbcType=VARCHAR} AS WQ_SFDBHN_YS
, SYSDATE AS MODIFY_TIME
FROM DUAL
</foreach>
) S
ON (T.ID = S.ID)
WHEN MATCHED THEN
UPDATE SET T.WQGRD = S.WQGRD, T.SFDB = S.SFDB, T.WQ_SFDBHN_YS = S.WQ_SFDBHN_YS, T.MODIFY_TIME = S.MODIFY_TIME
</update>
</mapper>

View File

@ -12,9 +12,9 @@ VITE_APP_BASE_API = '/dev-api'
# 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

@ -0,0 +1,417 @@
# 地图模块 3D Cesium 实施计划
## 1. 目标
在现有地图体系下补齐 `3D(Cesium)` 视图能力,使其尽量复用 `2D(OpenLayers)` 的业务编排结果,满足以下要求:
- 3D 默认进入中国视角,不再落到美国区域。
- 3D 初始化时显示过渡动画,再定位到中国。
- 3D 保留基础底图加载能力,但不提供底图切换面板。
- 3D 支持锚点展示。
- 3D 支持与 2D 一致的鼠标悬停 popup样式复用现有 popup。
- 3D 支持与 2D 一致的锚点点击行为,点击后打开同一套详情弹框逻辑。
- 菜单切换、图层树勾选、图例勾选后3D 锚点与 2D 一样联动变化。
- 3D 必须复用 `eng_point` 的层级显示规则,中型水电站仅在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。
## 2. 2D 现状梳理
当前 `2D` 的能力并不是散落在页面里,而是已经形成了比较清晰的分层:
### 2.1 页面入口
- `src/components/gis/GisView.vue`
- 负责挂载地图容器、popup 容器、地图控制器、图例、图层树。
- 页面只调用 `mapOrchestrator.mountView()``handlePageChange()`,不直接拼地图业务。
### 2.2 地图编排层
- `src/modules/map/application/map-orchestrator.ts`
- 负责页面初始化、菜单切换、图层配置加载、图例配置加载、基地切换、缩放联动。
- 2D/3D 都应尽量复用这一层,避免在 Cesium 侧另起一套业务流程。
### 2.3 地图统一门面
- `src/components/gis/map.class.ts`
- 对外暴露统一接口,如:
- `addBaseDataLayer`
- `addInitDataLayer`
- `mdLayerTreeShowOrHidden`
- `setLegendPointVisible`
- `controlBaseLayerTreeShowAndHidden`
- `initPopupOverlay`
- `flyTopanto`
- 业务层只认这套接口,不应感知底层是 `OpenLayers` 还是 `Cesium`
### 2.4 2D 地图能力落点
- `src/components/gis/map.ol.ts`
- 已实现的关键能力:
- 默认中国视角初始化
- 基础底图加载与切换
- 点位图层注册和显隐
- 图例控制单类锚点显隐
- 鼠标 hover popup
- click 打开详情弹窗
- 基地裁切与底图遮罩
- 缩放/移动后的批量 popup 和标签刷新
### 2.5 2D 锚点与 popup 支撑
- `src/components/gis/ol/point-layer-manager.ts`
- 负责将点位数据转成地图要素、分层、索引、图例显隐。
- `src/components/gis/ol/popup-manager.ts`
- 负责 hover 命中、popup DOM 内容复用、批量 popup。
### 2.6 图层树和图例交互
- `src/components/mapController/LayerController.vue`
- 图层树勾选后走 `mapOrchestrator.applyLayerSelection()`
- `src/components/mapLegend/index.vue`
- 图例点击后走 `mapOrchestrator.toggleLegend()` / `toggleLegendBatch()`
- `src/store/modules/map.ts`
- 真正把缓存数据、图层状态、图例状态应用到地图实例
- 也就是说,`3D` 只要把统一接口补齐,就能直接接住现有业务流
### 2.7 2D 已有特殊业务规则
- 锚点点击逻辑已在 `src/components/gis/map.ol.ts` 实现:
- 命中锚点后,`ylfb` 走 `ylfbModalVisible`
- 其他锚点走 `modalVisible`
- 标题和参数都直接取当前锚点数据
- `eng_point` 的中型水电站显示阈值已在现有链路中实现:
- 阈值常量位于 `src/modules/map/application/map-orchestrator.ts`
- 过滤逻辑位于 `src/store/modules/map.ts``filterPointLayerDataForDisplay()`
- `3D` 不应重新发明一套规则,而应复用这套现有过滤结果
- 部分点位会随着缩放级别提升显示得更多,不是简单的“图层开/关”:
- `eng_point` 的中型电站要到 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上才显示
- 水电开发页在缩放跨过 `12` 级时会自动补开一批动态图层
- `2D` 内部还存在点位密度阈值、近邻点展开、批量 popup 阈值等缩放联动规则
- `2D` 对锚点存在碰撞检测与密度控制,不是所有点始终全量显示:
- 图标和标签会分别经过碰撞计算
- 低缩放下会因为密度、碰撞、区域裁切、图例状态而隐藏部分点
- 高缩放下会逐步放开限制,显示更多锚点和标签
## 3. 3D 当前现状与缺口
### 3.1 已有内容
- `src/components/gis/map.cesium.ts`
- 当前只完成了最小化 `Cesium.Viewer` 初始化。
- 已有基础缩放、截图、`flyTopanto()` 雏形。
### 3.2 当前主要缺口
- `init()` 里虽然定义了 `flyToChina()`,但没有真正执行,也没有 `resolve(viewer)`,初始化流程不完整。
- 默认底图仍是 ArcGIS 全球影像,因此启动时容易落到美国默认视角。
- `addBaseDataLayer()` 为空,无法接住当前 `GISMap/customBaseLayer` 逻辑。
- `addInitDataLayer()` 为空,无法展示锚点。
- `mdLayerTreeShowOrHidden()` 为空,菜单切换时 3D 锚点不会同步显隐。
- `setLegendPointVisible()` 为空,图例勾选不会影响 3D 锚点。
- `initPopupOverlay()` 为空,无法复用 2D popup 容器。
- 缺少点击命中与详情弹框联动3D 当前无法做到“点击锚点与 2D 同行为”。
- `controlBaseLayerTreeShowAndHidden()` 为空3D 基础底图和叠加层无法受现有图层树控制。
- 尚未验证 `eng_point` 的缩放阈值规则在 3D 下是否能完全复用,存在实现时遗漏中型站点显隐的风险。
- 目前没有承接 `2D` 中“放大到具体层级显示更多锚点”的缩放联动显示机制。
- 目前没有承接 `2D` 中图标/标签的碰撞检测逻辑,后续若直接全量渲染,视觉效果会与 `2D` 明显不一致。
- 当前没有建立 `Cesium entity / datasource / imageryLayer` 的注册表,后续联动无从下手。
## 4. 实施原则
- 不重做业务编排,优先复用 `map-orchestrator.ts``src/store/modules/map.ts`
- 不额外引入一套 3D 专用图例和图层状态,仍以现有 store 为唯一运行态来源。
- 不单独复制 `eng_point` 的中型站显示规则,优先复用现有 `mapStore` 过滤结果。
- 不单独新建一套点击弹框协议,优先保持与 `map.ol.ts` 相同的命中后行为。
- 对“缩放越大显示越多锚点”这类规则,优先复用现有缩放阈值与显示门闩,不做拍脑袋的新规则。
- 对碰撞检测,优先与 `2D` 保持同方向行为:低缩放控量,高缩放放开,而不是 3D 全量堆点。
- 先做最小闭环,再逐步补能力:
- 第一步保证 3D 能正确初始化并定位中国。
- 第二步保证基础底图能显示。
- 第三步保证锚点能显示。
- 第四步保证 popup、图层树、图例联动打通。
- 3D 不做“基础地图切换 UI”但底层仍要支持 `customBaseLayer + addBaseDataLayer(..., true)` 这一逻辑。
## 5. 分步骤实施计划
### Step 1修正 3D 初始化并默认定位中国
目标:
- 切到 3D 后稳定初始化 viewer。
- 首屏出现一个轻量加载动画。
- 初始化完成后飞到中国默认视角。
计划:
- 修复 `map.cesium.ts``init()`
- 保证初始化后一定 `resolve(this.viewer)`
- 执行 `flyToChina()`
- 补一个可控的启动过渡动画。
- 统一中国默认参数:
- 中心经纬度建议复用 `104.5, 36.5`
- 相机高度先按全国视角收口,后续再微调
- 如果切回 2D 再切到 3D仍然要能正确重建 viewer。
验收:
- 点击 `3D` 按钮后,不再出现美国默认视角。
- 首屏有加载过渡。
- 进入后稳定落在中国。
### Step 2展示 3D 基础底图
目标:
- 3D 不显示空球。
- 保留“基础底图 + 3D 专用底图地址”能力。
- 不提供底图切换面板,但底层能吃现有配置。
计划:
- 在 `MapCesium` 内建立基础图层注册表。
- 实现 `addBaseDataLayer(layer, isShow)`
- 对 `customBaseLayer` 使用 `url_3d`
- 对 `BASEMAP-img`、`BASEMAP-white` 预留兼容能力
- 实现 `controlBaseLayerTreeShowAndHidden()`
- 让图层树勾选能控制 3D 底图显隐
- 根据你的要求UI 层先不开放 `BaseLayerSwitcher` 的 3D 切换行为,只保留默认基础底图加载。
验收:
- 切到 3D 后可稳定看到基础底图。
- 图层初始化时 `addBaseDataLayer(this.baseLayerConfig, checked, true)` 的思路在 3D 可落地。
### Step 3打通 3D 锚点展示
目标:
- 3D 能展示和 2D 同源的锚点数据。
- 菜单切换后3D 能跟随页面数据切换。
计划:
- 在 `MapCesium` 内建立点位图层注册结构,建议按 `layerKey -> entity[]``CustomDataSource` 管理。
- 实现 `addInitDataLayer(pointData, layerType)`
- 复用当前点位数据结构
- 解析 `lgtd/lttd/iconCode/titleName/stnm/anchoPointState/popupHtml`
- 复用现有图标资源路径
- 确保 `eng_point` 仍按现有 `mapStore` 过滤后的数据展示,中型站点只在 `7.5` 以上层级出现
- 为后续缩放驱动的增量显示预留运行态字段,不把所有点简单一次性硬渲染
- 实现 `mdLayerTreeShowOrHidden(layerType, checked)`
- 图层树控制整个点图层显隐
- 实现 `removePointLayer()` / `hasLayer()`
- 便于后续页面切换和重载
验收:
- 切到 3D 后当前页默认勾选图层的锚点能显示。
- 切换菜单后3D 锚点跟随变化。
- 取消勾选图层后,对应锚点消失。
- `eng_point` 在低于 `7.5` 时不显示中型水电站,高于 `7.5` 时正常显示。
### Step 4打通 3D popup 与 hover
目标:
- 3D 鼠标悬停时显示与 2D 一样的 popup。
- popup 样式复用现有 DOM 和样式,不再单独做一版。
计划:
- 实现 `initPopupOverlay(popupContainer)`
- 继续复用 `GisView.vue` 中现有 popup 容器
- 在 Cesium 中增加鼠标移动命中:
- 基于 `ScreenSpaceEventHandler`
- hover 到锚点时读取 entity 附带属性
- popup 内容生成逻辑复用 2D 现有字段:
- 优先使用 `popupHtml`
- 否则走通用字段渲染
- 处理离开锚点时隐藏 popup。
验收:
- 鼠标移入锚点出现 popup。
- 样式和 2D 保持一致。
- 移出后 popup 正常消失。
### Step 5补齐缩放驱动显示与碰撞检测
目标:
- 3D 在低缩放下控制锚点数量,避免全量堆叠。
- 3D 在放大到具体层级后,逐步显示更多锚点。
- 3D 的图标/标签显示效果尽量接近 2D不出现严重遮挡。
计划:
- 先承接现有明确可复用的缩放规则:
- `eng_point``7.5` 阈值
- 水电开发页的 `12` 级动态图层联动
- 再补 3D 侧的显示门闩:
- 低缩放下根据缩放级别、图层状态、图例状态控制 entity 显隐
- 高缩放下逐步放开限制,显示更多锚点
- 为 3D 补一版轻量碰撞控制:
- 至少先控制标签碰撞,避免名称完全叠在一起
- 如有必要,再补图标级碰撞或屏幕网格抽稀
- 明确与 2D 对齐的目标不是“算法完全一样”,而是“视觉行为一致”:
- 低缩放控量
- 高缩放增量显示
- 关键锚点不被完全淹没
验收:
- 3D 在全国视角下不会一次性挤满所有锚点。
- 放大到具体层级后会看到更多锚点逐步出现。
- 标签和锚点不会大面积重叠到不可读。
### Step 6打通 3D 锚点点击详情
目标:
- 3D 点击锚点后,行为与 2D 一致。
- 继续复用现有详情弹框状态和参数结构。
计划:
- 在 Cesium 中增加点击命中:
- 基于 `ScreenSpaceEventHandler`
- 命中锚点后读取 entity 绑定的原始业务数据
- 复用 `2D` 当前点击行为:
- `sttpMap === 'ylfb'` 时打开 `ylfbModalVisible`
- 其他锚点打开 `modalVisible`
- `params`、`title` 赋值逻辑与 `map.ol.ts` 保持一致
- 避免 hover popup 与 click 弹框互相干扰。
验收:
- 点击 3D 锚点后可打开与 2D 一致的详情弹框。
- `ylfb` 和普通锚点分支行为一致。
- 点击空白区域不会误触发弹框。
### Step 7打通图例联动
目标:
- 图例勾选后3D 只显示对应状态的锚点。
计划:
- 实现 `setLegendPointVisible(layerKey, anchoPointState, checked)`
- 复用 `anchoPointState` 作为图例匹配字段
- entity 级别维护显示状态
- 如果后续需要,也可补一个 3D 版本的图例索引表,避免每次全量遍历。
验收:
- 图例单项勾选/取消时3D 对应类别锚点同步显隐。
- 图例分组勾选/取消时3D 行为与 2D 保持一致。
### Step 8补足收口与稳定性
目标:
- 让 3D 能稳定参与现有地图生命周期。
计划:
- 清理 Cesium 事件监听、viewer、entity、datasource避免重复切换内存泄漏。
- 检查 `switchView('2D' | '3D')` 前后 popup、底图、图层注册是否被正确重置。
- 检查 hover、click、图例、图层树、缩放阈值这几条联动链路在 3D 是否完全闭环。
- 视情况补最小诊断日志,确认菜单切换与图层联动没有漏项。
验收:
- 2D/3D 多次来回切换不报错。
- 菜单切换、图层切换、图例切换行为稳定。
## 6. 推荐落地顺序
按你的要求,建议严格按下面顺序推进:
1. 默认定位到中国 + 启动动画
2. 展示基础底图
3. 展示锚点
4. 补 hover popup
5. 补缩放驱动显示与碰撞检测
6. 补锚点点击详情
7. 补图层树联动
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`
- 3D 地图主体实现,主改动文件
- `src/components/gis/map.class.ts`
- 视情况补齐 2D/3D 切换后 popup 初始化和服务切换细节
- `src/components/gis/GisView.vue`
- 视情况控制 3D 下的底图切换器显示策略、加载态挂载位置
- `src/components/mapController/index.vue`
- 若需要补 3D 初始化后的交互控制
## 8. 当前建议
建议先只做 `Step 1`,也就是:
- 修正 Cesium 初始化完成逻辑
- 增加 3D 启动加载动画
- 默认飞到中国
这一步最小、最容易验证,也能先把“打开 3D 看起来不对”的核心问题解决掉。

View File

@ -1,43 +0,0 @@
# 问题汇总
> 本记录用于跟踪项目开发过程中发现的问题,按日期倒序排列,并标注每项问题的发现人及处理状态。
---
## 问题列表2026-06-26
| 发现日期 | 解决人 | 编号 | 问题描述 | 状态 | 备注 |
| ---------- | ------ | ---- | ---------------------------------------------------------------------- | ------ | -------- |
| 2026-06-26 | 扈 | 1 | 基础信息图片展示是什么逻辑 | ✅ | <br /> |
| 2026-06-26 | 扈 | 2 | 电站专题展示逻辑 | ✅ | <br /> |
| 2026-06-26 | <br /> | 3 | 实时视频回放 少接口 | ⚠ | 缺少接口 |
| 2026-06-26 | <br /> | 4 | 预警提示 少接口 | ⚠ | 缺少接口 |
| 2026-06-26 | 扈 | 5 | 生态流量 达标率查询不对 | ✅ | <br /> |
| 2026-06-26 | 王 | 6 | 鱼类适应性繁殖同期对比 NAN | ⚠ | <br /> |
| 2026-06-26 | 扈 | 7 | 出库水温 综合分析 导出没做 | ✅ | <br /> |
| 2026-06-26 | <br /> | 8 | 栖息地-流量监测 没有水位视频和流量视频 字段没数据 | ⚠ | <br /> |
| 2026-06-26 | <br /> | 9 | 栖息地 水温、水质、流量没有数据没法测 | ⚠ | <br /> |
| 2026-06-26 | 扈 | 10 | 鱼类增殖站 - 运行数据 要添加字典 | ✅ | <br /> |
| 2026-06-26 | 扈 | 11 | 珍惜植物园 - 种植要求字段不知道 | ✅ | <br /> |
| 2026-06-26 | 扈 | 12 | 水生调查断面 - 监测数据 不知道电导率,性腺发育期,早期资源量和种类 | ✅ | <br /> |
| 2026-06-26 | 扈 | 13 | 水电告警情况 生态流量 不知道字段 接口传参有问题 | ✅ | <br /> |
| 2026-06-26 | <br /> | 14 | 地图抽吸 碰撞检测 放大到具体层级锚点抽吸了但是 popup 弹框还在显示 | ⚠ | <br /> |
| 2026-06-26 | <br /> | 15 | 地图比如积石峡 放大到层级被公伯峡隐藏掉了,然后在放大才显示 抽吸的问题 | ⚠ | <br /> |
| 2026-06-26 | <br /> | 16 | 运行情况 计划开始运行时间 接口没接 | ⚠ | <br /> |
| 2026-07-01 | 王 | 17 | 所有弹框加拖动 | ✅ | <br /> |
| 2026-07-01 | 王 | 18 | 所有弹框检查宽度有的是固定的 | ✅ | <br /> |
| 2026-07-01 | 王 | 18 | 所有导出的添加 | <br /> | <br /> |
| 2026-07-02 | 扈 | 19 | 地图点击基地切换偶发性会全部展示出来所有锚点 | <br /> | <br /> |
| 2026-07-03 | 汤 | 20 | 图例数据关了选中配置需要添加一个字段是默认不显示 | <br /> | <br /> |
---
## 状态说明
- ✅ 已解决 / 已确认
- ⚠ 处理中 / 待解决
- ❌ 阻塞 / 未开始
---

View File

@ -62,7 +62,7 @@ export function checkAnalysisShow(data: any) {
// 查询站点的关联子站点 (垂向水温、AI 识别)
export function getStInfoByStcd(params: any) {
return request({
url: '/api/dec-lygk-base-server/base/msstbprpt/getStInfoByStcd',
url: '/eng/base/msstbprpt/getStInfoByStcd',
method: 'get',
params
});

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

@ -0,0 +1,100 @@
import request from '@/utils/request';
// 获取列表
export function getPushConfigList(data: any) {
return request({
url: '/system/pushConfig/getPushConfigList',
method: 'post',
data
});
}
//新增推送配置
export function addPushConfig(data: any) {
return request({
url: '/system/pushConfig/addPushConfig',
method: 'post',
data
});
}
//修改新增配置
export function deletePushConfig(data: any) {
return request({
url: '/system/pushConfig/deletePushConfig',
method: 'post',
data
});
}
//删除新增配置
export function updatePushConfig(data: any) {
return request({
url: '/system/pushConfig/updatePushConfig',
method: 'post',
data
});
}
//推送目标配置 --用户
export function queryUsersList(data: any) {
return request({
url: '/system/pushConfig/queryUsersList',
method: 'get',
params:data
});
}
//推送目标配置 -- 角色
export function queryRolesList(data: any) {
return request({
url: '/system/pushConfig/queryRolesList',
method: 'get',
params:data
});
}
//查询推送目标
export function getPushTargets(data: any) {
return request({
url: '/system/pushConfig/getPushTargets',
method: 'get',
params:data
});
}
//新增推送目标
export function savePushTargets(data: any) {
return request({
url: '/system/pushConfig/savePushTargets',
method: 'post',
data
});
}
//短信提醒状态修改
export function setPushConfigSmsSwitch(data: any) {
return request({
url: '/system/pushConfig/setPushConfigSmsSwitch',
method: 'post',
params:data
});
}
//获取推送历史列表
export function postPushHistoryList(data: any) {
return request({
url: '/system/pushConfig/postPushHistoryList',
method: 'post',
data
});
}
//推送情况
export function getPushHistoryDetailList(data: any) {
return request({
url: '/system/pushConfig/getPushHistoryDetailList',
method: 'post',
data
});
}
//s数量
export function getPushHistoryDetailById(data: any) {
return request({
url: '/system/pushConfig/getPushHistoryDetailById',
method: 'get',
params:data
});
}

View File

@ -115,7 +115,7 @@
<script lang="ts" setup>
import dayjs from 'dayjs';
import { ref, onMounted, nextTick, watch, computed } from 'vue';
import { ref, nextTick, watch, computed } from 'vue';
import {
BasicColumns,
wtPointColumns,
@ -359,8 +359,7 @@ const getData = async () => {
try {
if (props.url && modelStore.params.stcd) {
let res: any = await getStcdDetail(props.url, modelStore.params.stcd);
console.log(res);
if (res && res.success) {
if (res && res.code) {
let data = res.data;
data2.value = data;

View File

@ -17,7 +17,7 @@ const ENGTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
// {
@ -113,7 +113,7 @@ const ENGEQTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -213,7 +213,7 @@ const EQTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
// {
@ -239,7 +239,7 @@ const FPTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
code: 'gyss.tabs.jcxx'
},
{
@ -311,7 +311,7 @@ const FPTabs1: Array<any> = [
key: 'basicInfo',
type: 'basic',
default: true, // 默认显示
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
url: '/eng/base/msstbprpt/getStcdInfo'
},
{
name: '实时视频',
@ -333,7 +333,7 @@ const FBTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
url: '/eng/base/msstbprpt/getStcdInfo'
},
{
name: '运行数据',
@ -442,7 +442,7 @@ const FHTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -476,7 +476,7 @@ const ZQTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -493,7 +493,7 @@ const WQFBTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
url: '/eng/base/msstbprpt/getStcdInfo'
},
{
name: '监测数据',
@ -547,7 +547,7 @@ const FLOWTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -562,7 +562,7 @@ const WTTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
url: '/eng/base/msstbprpt/getStcdInfo'
},
{
name: '监测数据',
@ -590,7 +590,7 @@ const WTTabs1: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
url: '/eng/base/msstbprpt/getStcdInfo'
},
{
name: '监测数据',
@ -608,7 +608,7 @@ const DWTabs: Array<any> = [
key: 'basicInfo',
type: 'basic',
default: true, // 默认显示
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo'
url: '/eng/base/msstbprpt/getStcdInfo'
},
{
name: '实时视频',
@ -668,7 +668,7 @@ const WETabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -729,7 +729,7 @@ const DEVICETABS: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
}
];
@ -739,7 +739,7 @@ const DZGJ: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -801,7 +801,7 @@ const VPTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
// {
@ -853,7 +853,7 @@ const videoTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -869,7 +869,7 @@ const noLiveVideoTabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -885,7 +885,7 @@ const VATabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
// {
@ -961,7 +961,7 @@ const WeatherTabs: any = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -969,9 +969,8 @@ const WeatherTabs: any = [
key: '',
type: '',
url: ''
},
]
}
];
// const WaterQualityMonitoring: any = [
// {
@ -1020,7 +1019,7 @@ const WATabs: Array<any> = [
name: '基础信息',
key: 'basicInfo',
type: 'basic',
url: '/api/dec-lygk-base-server/base/msstbprpt/getStcdInfo',
url: '/eng/base/msstbprpt/getStcdInfo',
default: true // 默认显示
},
{
@ -1233,7 +1232,7 @@ const handleTabs = (modaldata: any) => {
// case 'WE':
// return WETabs
case 'MM':
return WeatherTabs
return WeatherTabs;
// case 'WARN':
// return WaterQualityMonitoring
// case 'AI':

View File

@ -9,33 +9,46 @@ import type { MapInterface, layer } from './map.d';
export class MapCesium implements MapInterface {
private viewer: Cesium.Viewer | null = null;
private containerId = '';
private containerElement: HTMLElement | null = null;
private loadingOverlayElement: HTMLDivElement | null = null;
private loadingSpinnerAnimation: Animation | null = null;
private flightFallbackTimer: number | null = null;
init(container: HTMLElement, rectangle?: any): Promise<any> {
return new Promise(async (resolve, reject) => {
private readonly CHINA_CENTER = {
lng: 114.5,
lat: 36.5
};
private readonly CHINA_BOOT_HEIGHT = 20000000;
private readonly CHINA_DEFAULT_HEIGHT = 12000000;
private readonly CHINA_VIEW_ORIENTATION = {
heading: Cesium.Math.toRadians(0),
pitch: Cesium.Math.toRadians(-90),
roll: 0
};
async init(container: HTMLElement, _rectangle?: any): Promise<any> {
try {
this.containerId = container.id;
// ✅ 2. 初始化 Viewer
// 在 1.141.0 中,建议先不传 imageryProvider或者使用默认的 Ion Imagery
// 如果不想用 Ion可以传一个空的 ImageryProviderCollection 或者稍后移除
this.viewer = new Cesium.Viewer(this.containerId, {
animation: false, // 隐藏动画
timeline: false, // 隐藏时间线
baseLayerPicker: false, // 隐藏底图选择器
fullscreenButton: false, // 隐藏全屏按钮
vrButton: false, // 隐藏VR按钮
geocoder: false, // 隐藏地址搜索框
homeButton: false, // 隐藏首页按钮
sceneModePicker: false, // 隐藏场景模式选择器
navigationHelpButton: false, // 隐藏导航帮助按钮
infoBox: false, // 隐藏信息框
selectionIndicator: false, // 隐藏选择指示器
shouldAnimate: true // 开启动画
this.containerElement = container;
this.showLoadingOverlay(container);
this.viewer = new Cesium.Viewer(container, {
animation: false,
timeline: false,
baseLayerPicker: false,
fullscreenButton: false,
vrButton: false,
geocoder: false,
homeButton: false,
sceneModePicker: false,
navigationHelpButton: false,
infoBox: false,
selectionIndicator: false,
shouldAnimate: true
});
// // 清空默认图层
const layers = this.viewer.imageryLayers;
layers.removeAll();
// // 添加 ArcGIS 底图(正确 URL
const arcgisUrl =
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/';
@ -43,57 +56,160 @@ export class MapCesium implements MapInterface {
arcgisUrl,
{
enablePickFeatures: false,
// 可选:设置最大层级,避免请求过细的瓦片
maximumLevel: 19
}
);
// ✅ 6. 【关键】将 Provider 添加到图层集合中
layers.addImageryProvider(provider);
// layers.addImageryProvider(provider);
// // ✅ 5. 场景优化
this.viewer.scene.globe.depthTestAgainstTerrain = false;
// // 启用高 DPI 支持(可选,根据性能需求)
this.viewer.useBrowserRecommendedResolution = true;
// ✅ 6. 飞行到中国视角
// 使用 setTimeout 确保 DOM 和 WebGL 上下文完全就绪
// setTimeout(() => {
// this.flyToChina();
// resolve(this.viewer);
// }, 100);
// 先把相机直接摆到中国上空,避免初始化阶段短暂闪到美国默认视角。
this.setBootChinaView();
await this.waitForViewerReady();
await this.flyToChina();
return this.viewer;
} catch (error) {
this.hideLoadingOverlay();
console.error('Cesium Init Critical Error:', error);
reject(error);
throw error;
}
});
}
private flyToChina() {
private setBootChinaView() {
if (!this.viewer) return;
// 取消之前的飞行动画(如果有)
this.viewer.camera.cancelFlight();
this.viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(
this.CHINA_CENTER.lng,
this.CHINA_CENTER.lat,
this.CHINA_BOOT_HEIGHT
),
orientation: this.CHINA_VIEW_ORIENTATION
});
}
private waitForViewerReady(): Promise<void> {
return new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.viewer?.scene.requestRender();
resolve();
});
});
});
}
private flyToChina(): Promise<void> {
return new Promise(resolve => {
if (!this.viewer) {
this.hideLoadingOverlay();
resolve();
return;
}
const finish = () => {
if (this.flightFallbackTimer !== null) {
window.clearTimeout(this.flightFallbackTimer);
this.flightFallbackTimer = null;
}
this.hideLoadingOverlay();
resolve();
};
this.viewer.camera.cancelFlight();
this.flightFallbackTimer = window.setTimeout(() => {
finish();
}, 2600);
this.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(104.5, 36.5, 4000000),
orientation: {
heading: Cesium.Math.toRadians(0),
pitch: Cesium.Math.toRadians(-90),
roll: 0.0
},
duration: 2
destination: Cesium.Cartesian3.fromDegrees(
this.CHINA_CENTER.lng,
this.CHINA_CENTER.lat,
this.CHINA_DEFAULT_HEIGHT
),
orientation: this.CHINA_VIEW_ORIENTATION,
duration: 1.8,
complete: finish,
cancel: finish
});
});
}
private showLoadingOverlay(container: HTMLElement) {
this.hideLoadingOverlay();
const overlay = document.createElement('div');
overlay.setAttribute('data-map-cesium-loading', 'true');
overlay.style.position = 'absolute';
overlay.style.inset = '0';
overlay.style.display = 'flex';
overlay.style.flexDirection = 'column';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.gap = '12px';
overlay.style.background =
'linear-gradient(180deg, rgba(7, 20, 36, 0.72) 0%, rgba(7, 20, 36, 0.46) 100%)';
overlay.style.backdropFilter = 'blur(2px)';
overlay.style.zIndex = '30';
overlay.style.pointerEvents = 'auto';
const spinner = document.createElement('div');
spinner.style.width = '36px';
spinner.style.height = '36px';
spinner.style.borderRadius = '50%';
spinner.style.border = '3px solid rgba(255, 255, 255, 0.25)';
spinner.style.borderTopColor = '#ffffff';
this.loadingSpinnerAnimation = spinner.animate(
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
{
duration: 900,
iterations: Infinity,
easing: 'linear'
}
);
const label = document.createElement('div');
label.textContent = '3D场景加载中...';
label.style.color = '#ffffff';
label.style.fontSize = '14px';
label.style.lineHeight = '20px';
label.style.letterSpacing = '0.5px';
label.style.textShadow = '0 1px 2px rgba(0, 0, 0, 0.35)';
overlay.appendChild(spinner);
overlay.appendChild(label);
container.appendChild(overlay);
this.loadingOverlayElement = overlay;
}
private hideLoadingOverlay() {
if (this.loadingSpinnerAnimation) {
this.loadingSpinnerAnimation.cancel();
this.loadingSpinnerAnimation = null;
}
if (this.loadingOverlayElement?.parentNode) {
this.loadingOverlayElement.parentNode.removeChild(
this.loadingOverlayElement
);
}
this.loadingOverlayElement = null;
}
destroy(): void {
if (this.flightFallbackTimer !== null) {
window.clearTimeout(this.flightFallbackTimer);
this.flightFallbackTimer = null;
}
this.hideLoadingOverlay();
if (this.viewer) {
// ✅ 关键:销毁前取消所有动画和事件
this.viewer.camera.cancelFlight();
this.viewer.destroy();
this.viewer = null;
}
this.containerElement = null;
}
flyTopanto(position: number[], zoom: number): void {
@ -113,37 +229,41 @@ export class MapCesium implements MapInterface {
}
// ... 其他空实现方法保持不变 ...
jdPanelControlShowAndHidden(baseid: string, isAll: boolean): void {}
jdPanelControlShowAndHidden(_baseid: string, _isAll: boolean): void {}
mdLayerShowOrHidden(): void {}
addBaseDataLayer(layer: any): void {}
addBaseDataLayer(_layer: any): void {}
controlBaseLayerTreeShowAndHidden(
layerType: string,
key: string,
checked: boolean
_layerType: string,
_key: string,
_checked: boolean
): void {}
mdLayerTreeShowOrHidden(layerType: string, checked?: boolean): void {}
hasLayer(layerKey: string): boolean {
mdLayerTreeShowOrHidden(_layerType: string, _checked?: boolean): void {}
hasLayer(_layerKey: string): boolean {
return false;
}
hasBaseLayer(layerKey: string): boolean {
hasBaseLayer(_layerKey: string): boolean {
return false;
}
setLegendPointVisible(
layerKey: string,
anchoPointState: string,
checked: boolean
_layerKey: string,
_anchoPointState: string,
_checked: boolean
): void {}
addInitDataLayer(pointData: any[], layerType: any, mdoptions?: any): void {}
switchView(type: any): void {}
fitBounds(bounds: any): void {}
baseLayerSwitcher(key: string): void {}
addInitDataLayer(
_pointData: any[],
_layerType: any,
_mdoptions?: any
): void {}
switchView(_type: any): void {}
fitBounds(_bounds: any): void {}
baseLayerSwitcher(_key: string): void {}
addTertiarybasinLayer(
layer: layer,
fillcolor: any,
outlineColor: any,
datas: any
_layer: layer,
_fillcolor: any,
_outlineColor: any,
_datas: any
): void {}
hideTertiarybasinLayer(layer: layer): void {}
hideTertiarybasinLayer(_layer: layer): void {}
zoomToggle(type: 'out' | 'in'): void {
if (!this.viewer) return;
const factor = 1.5;
@ -195,7 +315,6 @@ export class MapCesium implements MapInterface {
// 清理临时元素
document.body.removeChild(link);
} catch (error) {
console.error('Failed to export map image:', error);
if (error instanceof DOMException && error.name === 'SecurityError') {
@ -207,5 +326,5 @@ export class MapCesium implements MapInterface {
}
// ... 其他代码 ...
initPopupOverlay(popupContainer: HTMLDivElement): void {}
initPopupOverlay(_popupContainer: HTMLDivElement): void {}
}

View File

@ -79,21 +79,21 @@ const data = ref([
name: '正常',
num: '0',
warnState: '1',
icon: 'iconfont icon-shuidianzhan1 zhuangji',
icon: 'iconfont icon-map-rgszzDabiao zhuangji',
background: '#78c300',
},
{
name: '告警',
num: '0',
warnState: '2',
icon: 'iconfont icon-shuidianzhanGaojing shebeiZhengchang',
icon: 'iconfont icon-map-rgszzDabiao shebeiZhengchang',
background: '#e55555',
},
{
name: '无数据',
num: '0',
warnState: '0',
icon: 'iconfont icon-shuidianzhanWushujv shebeiLixian',
icon: 'iconfont icon-map-rgszzDabiao shebeiLixian',
background: '#8d8d8d',
},
]);

View File

@ -54,7 +54,6 @@ service.interceptors.response.use(
(response: any) => {
const { status, msg } = response;
if (status === 200) {
console.log(response.data);
if (response.data.code == 401) {
message.error(response.data.msg || '请求失败');
setTimeout(() => {

View File

@ -22,7 +22,7 @@
<a-button type="link" @click="backToLogin" class="back-button">
<span> 返回</span>
</a-button>
<h3 class="forgot-password-title">忘记密码</h3>
<!-- <h3 class="forgot-password-title">忘记密码</h3> -->
</div>
<a-form
@ -32,7 +32,11 @@
class="form-container"
>
<a-form-item label="" name="phone">
<a-input placeholder="请输入手机号" size="large">
<a-input
v-model:value="forgotPasswordForm.phone"
placeholder="请输入手机号"
size="large"
>
<template #prefix>
<MobileOutlined />
</template>
@ -42,9 +46,10 @@
<a-form-item label="" name="captcha">
<div class="captcha-row">
<a-input
v-model="forgotPasswordForm.captcha"
v-model:value="forgotPasswordForm.captcha"
placeholder="请输入验证码"
size="large"
autocomplete="current-captcha"
class="captcha-input"
>
<template #prefix>
@ -69,7 +74,8 @@
<a-form-item label="" name="newPassword">
<a-input-password
v-model="forgotPasswordForm.newPassword"
v-model:value="forgotPasswordForm.newPassword"
autocomplete="current-password"
placeholder="请输入新密码"
size="large"
>
@ -79,6 +85,19 @@
</a-input-password>
</a-form-item>
<a-form-item label="" name="confirmPassword">
<a-input-password
v-model:value="forgotPasswordForm.confirmPassword"
autocomplete="current-confirmPassword"
placeholder="请再次输入密码"
size="large"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-button
type="primary"
size="large"
@ -162,32 +181,51 @@
>
<span>登录</span>
</a-button>
<!-- <a-button
<div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<a-checkbox v-model:checked="remember" style="margin-top: 10px"
>记住密码</a-checkbox
>
<a-button
type="link"
size="mini"
block
@click="showForgotPasswordPage"
:style="{ marginTop: '10px', border: 'none' }"
>
忘记密码
</a-button>
<!-- <a-button
type="link"
size="mini"
@click="goRegister"
:style="{ marginTop: '10px', border: 'none' }"
>
用户注册
</a-button> -->
</div>
<!-- 忘记密码 -->
</a-form>
</a-tab-pane>
<!-- 短信登录 Tab (占位) -->
<!-- <a-tab-pane key="sms" tab="短信登录">
<a-tab-pane key="sms" tab="短信登录">
<a-form
:model="loginData"
:rules="loginRules"
:rules="state.smsLoginRules"
layout="vertical"
class="form-container"
@finish="onSmsLogin"
>
<a-form-item label="" name="username">
<a-input
v-model:value="loginData.username"
autocomplete="current-username"
placeholder="请输入手机号"
size="large"
>
@ -199,9 +237,8 @@
<a-form-item label="" name="captcha">
<div class="captcha-row">
<a-input
v-model="loginData.code"
v-model:value="loginData.code"
placeholder="请输入验证码"
autocomplete="current-captcha"
size="large"
class="captcha-input"
>
@ -235,8 +272,36 @@
>
<span>登录</span>
</a-button>
<div
style="
width: 100%;
display: flex;
align-items: center;
justify-content: start;
"
>
<a-button
type="link"
size="mini"
block
@click="showForgotPasswordPage"
:style="{ marginTop: '10px', border: 'none' }"
style="width: 20%;"
>
忘记密码
</a-button>
<!-- <a-button
type="link"
size="mini"
block
@click="goRegister"
:style="{ marginTop: '10px', border: 'none' }"
>
注册
</a-button> -->
</div>
</a-form>
</a-tab-pane> -->
</a-tab-pane>
</a-tabs>
</div>
</div>
@ -248,19 +313,23 @@
import { onMounted, reactive, ref, toRefs, watch } from "vue";
import loginImg from "@/assets/images/logo.png";
import { UserOutlined, LockOutlined, MobileOutlined } from "@ant-design/icons-vue";
import { getCaptcha } from "@/api/auth";
import { getCaptcha, sendSmsCode, smsLoginApi, resetPassword } from "@/api/auth";
import { message } from "ant-design-vue";
import { setToken } from "@/utils/auth";
import Cookies from "js-cookie";
//
import router from "@/router";
// API
import { useRoute } from "vue-router";
import { LoginData } from "@/api/auth/types";
//
import { encrypt } from "@/utils/rsaEncrypt";
import { encrypt, decrypt } from "@/utils/rsaEncrypt";
//
import { useUserStore } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission";
//
import { useI18n } from "vue-i18n";
@ -280,6 +349,7 @@ const forgotPasswordForm = ref({
phone: "",
captcha: "",
newPassword: "",
confirmPassword: "",
});
//
const showForgotPassword = ref(false);
@ -299,6 +369,10 @@ const state = reactive({
password: [{ required: true, trigger: "blur", message: t("login.rulesPassword") }],
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
},
smsLoginRules: {
username: [{ required: true, trigger: "blur", message: "请输入手机号" }],
code: [{ required: true, trigger: "blur", message: "请输入验证码" }],
},
loginImg: loginImg[0],
loading: false,
passwordType: "password",
@ -320,15 +394,80 @@ const forgotPasswordRules = ref({
captcha: [{ required: true, message: "验证码不能为空", trigger: "blur" }],
newPassword: [
{ required: true, message: "新密码不能为空", trigger: "blur" },
{ min: 6, message: "密码长度不能少于6位", trigger: "blur" },
{ min: 10, message: "密码长度不能少于10位", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (!value) return Promise.resolve();
//
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(
Boolean
).length;
if (typeCount < 3) {
return Promise.reject(
new Error("密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种")
);
}
//
if (/(.)\1{1,}/.test(value)) {
return Promise.reject(new Error("密码不能包含连续重复字符"));
}
// /
for (let i = 0; i < value.length - 1; i++) {
if (/\d/.test(value[i]) && /\d/.test(value[i + 1])) {
const diff = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (Math.abs(diff) === 1) {
return Promise.reject(new Error("密码不能包含连续递增或递减的数字"));
}
}
}
// /
for (let i = 0; i < value.length - 1; i++) {
if (/[a-zA-Z]/.test(value[i]) && /[a-zA-Z]/.test(value[i + 1])) {
const diff = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (Math.abs(diff) === 1) {
return Promise.reject(new Error("密码不能包含连续递增或递减的字母"));
}
}
}
return Promise.resolve();
},
trigger: "blur",
},
],
confirmPassword: [
// { required: true, message: "", trigger: "blur" },
{
validator: (rule: any, value: string) => {
if (!value) {
return Promise.reject(new Error("请再次输入密码"));
}
if (value !== forgotPasswordForm.value.newPassword) {
return Promise.reject(new Error("两次输入的密码不一致"));
}
return Promise.resolve();
},
trigger: "blur",
},
],
});
const { loginData, loginRules, loading,
const {
loginData,
loginRules,
loading,
// passwordType, capslockTooltipDisabled
} = toRefs(
state
);
} = toRefs(state);
// function checkCapslock(e: any) {
// const { key } = e;
@ -364,8 +503,19 @@ function onFinish() {
userStore
.login(user)
.then(() => {
if (remember.value == true) {
Cookies.set("username", user.username, { expires: 30 });
Cookies.set("password", user.password, { expires: 30 });
Cookies.set("rememberMe", String(remember.value), { expires: 30 });
} else {
// cookie
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
router.push({ path: "/" });
state.loading = false;
message.success("登录成功");
})
.catch(() => {
getCode();
@ -373,6 +523,93 @@ function onFinish() {
});
}
/**
* 短信验证码登录
*/
const onSmsLogin = async () => {
try {
// ========== ==========
const phoneRegex = /^1[3-9]\d{9}$/;
if (!loginData.value.username || !phoneRegex.test(loginData.value.username)) {
message.error("请输入正确的手机号");
return;
}
if (!loginData.value.code || loginData.value.code.trim() === "") {
message.error("请输入验证码");
return;
}
// ========== ==========
state.loading = true;
// ========== API ==========
console.log("开始调用短信登录API, 手机号:", loginData.value.username);
const res: any = await smsLoginApi(
loginData.value.username, //
loginData.value.code //
);
console.log("短信登录API响应:", res);
// ========== ==========
console.log("检查响应结果, res.code:", res.code, "类型:", typeof res.code);
// code "0" 0
if (res.code !== 0 && res.code !== "0") {
console.error("短信登录失败, 错误码:", res.code, "错误信息:", res.msg);
message.error(res.msg || "登录失败");
return;
}
// ========== Token ==========
console.log("开始提取Token, res.data:", res.data);
const token = res.data?.token || res.data; //
if (!token) {
console.error("Token为空, res.data结构:", JSON.stringify(res.data));
message.error("登录失败:未获取到Token");
return;
}
console.log("Token获取成功:", token);
// Cookie
setToken(token);
// Store Token
userStore.Token = token;
console.log("Token已存储到Store");
// ========== ==========
console.log("开始获取用户信息...");
const userInfo = await userStore.getInfo();
console.log("用户信息获取成功:", userInfo);
const roles = userInfo.roles;
console.log("用户角色:", roles);
// ========== ==========
console.log("开始生成路由...");
const permissionStore = usePermissionStoreHook();
const accessRoutes: any = await permissionStore.generateRoutes(roles);
console.log("路由生成成功, 路由数量:", accessRoutes.length);
// ========== ==========
accessRoutes.forEach((route: any) => {
router.addRoute(route);
});
// ========== ==========
router.push({ path: accessRoutes[0].children[0].opturl });
message.success("登录成功");
} catch (error: any) {
// ========== ==========
console.error("短信登录失败", error);
message.error(error.message || "登录失败,请重试");
} finally {
// ========== ==========
state.loading = false;
}
};
watch(
route,
() => {
@ -395,6 +632,22 @@ function getOtherQuery(query: any) {
return acc;
}, {});
}
function getCookie() {
const username = Cookies.get("username");
let password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
rememberMe == "true" ? (remember.value = Boolean(rememberMe)) : false;
// cookie
state.cookiePass = password === undefined ? "" : password;
password = password === undefined ? state.loginData.password : password;
state.loginData = {
username: username === undefined ? state.loginData.username : username,
password: decrypt(password),
code: "",
uuid: "",
};
remember.value = rememberMe === undefined ? false : Boolean(rememberMe);
}
function getCode() {
getCaptcha().then((result: any) => {
codeUrl.value = result.data.img;
@ -420,7 +673,10 @@ const startCountdown = () => {
const showForgotPasswordPage = () => {
showForgotPassword.value = true;
};
//
const goRegister = () => {
router.push({ path: "/register" });
};
//
const backToLogin = () => {
showForgotPassword.value = false;
@ -429,6 +685,7 @@ const backToLogin = () => {
phone: "",
captcha: "",
newPassword: "",
confirmPassword: "",
};
};
@ -456,10 +713,13 @@ const sendSms = async () => {
smsButtonDisabled.value = true;
try {
//
// await axios.post('/sms/send', { phone: loginForm.value.username })
//
// type1
const res: any = await sendSmsCode(loginData.value.username, 3);
if (res.code == 1) {
message.success(res.msg);
return;
}
//
message.success("验证码发送成功");
//
@ -496,17 +756,23 @@ const sendForgotPasswordSms = async () => {
smsButtonDisabled.value = true;
try {
//
// await axios.post('/sms/forgot-password', { phone: forgotPasswordForm.value.phone })
// type=3
const res: any = await sendSmsCode(forgotPasswordForm.value.phone, 2);
//
if (res.code == 1) {
message.error(res.msg);
smsButtonDisabled.value = false;
return;
}
//
message.success("验证码发送成功");
//
startCountdown();
} catch (error) {
console.error("发送验证码失败", error);
message.error("验证码发送失败,请重试");
} catch (error: any) {
// console.error("", error);
// message.error(error.message || "");
smsButtonDisabled.value = false;
} finally {
loading.value = false;
@ -514,23 +780,117 @@ const sendForgotPasswordSms = async () => {
};
//
const handleResetPassword = async () => {
loading.value = true;
//
try {
// 2. ()
// await axios.post('/user/reset-password', {
// phone: forgotPasswordForm.value.phone,
// captcha: forgotPasswordForm.value.captcha,
// newPassword: forgotPasswordForm.value.newPassword
// })
//
const phoneRegex = /^1[3-9]\d{9}$/;
if (
!forgotPasswordForm.value.phone ||
!phoneRegex.test(forgotPasswordForm.value.phone)
) {
message.error("请输入正确的手机号");
return;
}
// 3.
message.success("密码重置成功");
if (!forgotPasswordForm.value.captcha) {
message.error("请输入验证码");
return;
}
// 4.
backToLogin();
if (!forgotPasswordForm.value.newPassword) {
message.error("请输入新密码");
return;
}
//
if (!forgotPasswordForm.value.confirmPassword) {
message.error("请再次输入密码");
return;
}
//
if (
forgotPasswordForm.value.newPassword !== forgotPasswordForm.value.confirmPassword
) {
message.error("两次输入的密码不一致");
return;
}
//
const password = forgotPasswordForm.value.newPassword;
if (password.length < 10) {
message.error("密码长度不能少于10位");
return;
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(
Boolean
).length;
if (typeCount < 3) {
message.error("密码必须包含大写字母、小写字母、数字、特殊字符中的至少三种");
return;
}
//
if (/(.)\1{1,}/.test(password)) {
message.error("密码不能包含连续重复字符");
return;
}
// /
for (let i = 0; i < password.length - 1; i++) {
if (/\d/.test(password[i]) && /\d/.test(password[i + 1])) {
const diff = password.charCodeAt(i + 1) - password.charCodeAt(i);
if (Math.abs(diff) === 1) {
message.error("密码不能包含连续递增或递减的数字");
return;
}
}
}
// /
for (let i = 0; i < password.length - 1; i++) {
if (/[a-zA-Z]/.test(password[i]) && /[a-zA-Z]/.test(password[i + 1])) {
const diff = password.charCodeAt(i + 1) - password.charCodeAt(i);
if (Math.abs(diff) === 1) {
message.error("密码不能包含连续递增或递减的字母");
return;
}
}
}
} catch (error) {
console.error("密码重置失败", error);
message.error("密码重置失败,请重试");
console.error("表单验证失败", error);
return;
}
loading.value = true;
try {
// RSA
const encryptedPassword = encrypt(forgotPasswordForm.value.newPassword);
// 使
const res: any = await resetPassword(
forgotPasswordForm.value.phone,
forgotPasswordForm.value.captcha,
encryptedPassword
);
if (res.code == 0) {
message.success("密码重置成功");
//
backToLogin();
} else {
message.error(res.msg || "密码重置失败");
}
} catch (error: any) {
// console.error("", error);
// message.error(error.message || "");
} finally {
loading.value = false;
}
@ -538,6 +898,7 @@ const handleResetPassword = async () => {
onMounted(() => {
getCode();
getCookie();
});
</script>
@ -635,10 +996,12 @@ onMounted(() => {
/* 验证码布局 */
.captcha-row {
display: flex;
.ant-row {
display: flex;
align-items: start;
}
/* gap: 12px;
align-items: center; */

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) => {

View File

@ -3,6 +3,8 @@
<div class="content">
<TiltPhotoManagementSearch
ref="tiltPhotoManagementSearch"
:river-options="riverOptions"
:river-options-load="riverOptionsLoad"
:handle-add="handleAdd"
@reset="handleReset"
@search-finish="onSearchFinish"
@ -36,6 +38,8 @@
<TiltPhotoManagementForm
ref="tiltPhotoManagementForm"
v-model:visible="editModalVisible"
:river-options="riverOptions"
:river-options-load="riverOptionsLoad"
:initial-values="currentRecord"
@cancel="editModalCancel"
@ok="handleEditSubmit"
@ -54,6 +58,7 @@ import {
saveTiltPhoto,
deleteTiltPhoto
} from '@/api/system/map/TiltPhotoManagement';
import { getRvcdDropdown } from '@/api/select';
//
const columns = [
{
@ -73,14 +78,14 @@ const columns = [
{
title: 'URL',
dataIndex: 'url',
key: 'url',
width: 360
key: 'url'
// width: 360
},
{
title: '所在河段',
dataIndex: 'hbrvcdName',
key: 'hbrvcdName',
width: 120
width: 280
},
{
title: '所属电站',
@ -91,7 +96,8 @@ const columns = [
{
title: '创建人',
key: 'recordUser',
dataIndex: 'recordUser'
dataIndex: 'recordUser',
width: 150
},
{
title: '创建时间',
@ -108,9 +114,9 @@ const columns = [
{
title: '操作',
key: 'action',
width: 80,
width: 120,
fixed: 'right',
slots: { customRender: 'status' }
slots: { customRender: 'action' }
}
];
@ -171,6 +177,12 @@ const onSearchFinish = (values: any) => {
operator: 'contains',
dataType: 'string',
value: values.stnm
},
{
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
].filter(Boolean)
};
@ -211,7 +223,20 @@ const handleEditSubmit = async (values: any) => {
return;
}
};
const initOption = async () => {};
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();
});

View File

@ -1,9 +1,9 @@
<template>
<a-modal
:title="isEdit ? '编辑沿程配置' : '新增沿程配置'"
:title="isEdit ? '修改编辑推送类型' : '新增编辑推送类型'"
v-model:open="modalVisible"
:confirm-loading="localLoading"
width="1200px"
width="35vw"
:destroy-on-close="true"
ok-text="保存"
@cancel="handleCancel"
@ -14,279 +14,105 @@
:model="formData"
:rules="rules"
name="edit_form"
:labelCol="{ span: 5 }"
:labelCol="{ span: 4 }"
>
<a-row>
<a-col :span="12">
<a-form-item label="沿程配置名称" name="name">
<a-col :span="24">
<a-form-item label="类型名称" name="messageType">
<a-input
v-model:value="formData.name"
v-model:value="formData.messageType"
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="请输入配置编码"
placeholder="请输入类型名称"
/> </a-form-item
></a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="所在河段" name="rvnm">
<a-col :span="24">
<a-form-item label="所属类别" name="categoryCode">
<a-select
v-model:value="formData.categoryCode"
placeholder="请选择"
style="width: 100%"
:options="categoryCodeOption"
@change="handleCategoryChange"
></a-select> </a-form-item
></a-col>
</a-row>
<a-row>
<a-col :span="24">
<a-form-item label="关联基地">
<a-select
v-model:value="formData.baseIdList"
mode="multiple"
style="width: 100%"
placeholder="请选择"
:options="baseIdsOption"
@change="handleBaseChange"
></a-select> </a-form-item
></a-col>
</a-row>
<a-row>
<a-col :span="24">
<a-form-item
label="定时任务"
style="
display: flex;
align-items: center;
justify-content: space-between;
"
>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
"
>
<a-input
v-model:value="formData.rvnm"
style="width: 100%"
placeholder="请输入所在河段"
/> </a-form-item
></a-col>
<a-col :span="12">
<a-form-item label="备注" name="description">
<a-textarea
:rows="4"
v-model:value="formData.description"
style="width: 100%"
placeholder="请输入备注"
/> </a-form-item
></a-col>
v-model:value="formData.cronExpression"
style="width: 70%"
placeholder="请输入cron表达式"
/>
<a-button style="width: 27%" @click="openCronModal"
>cron表达式</a-button
>
</div>
</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-modal>
<!-- 树形结构 -->
<a-tree
v-if="treeData.length > 0"
:tree-data="treeData"
:default-expand-all="false"
:selectable="false"
block-node
style="width: 300px"
<a-modal
title="Cron表达式选择器"
v-model:open="cronModalVisible"
width="1200px"
:destroy-on-close="true"
footer=""
@cancel="closeCronModal"
>
<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"
>
<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.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="添加子节点"
>
<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 stationList"
:key="item.stcd"
:value="item.stcd"
>
{{ item.stnm }}
</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>
<CronPicker
v-model="formData.cronExpression"
@confirm="handleCronConfirm"
/>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { ref, reactive, computed, watch,onMounted } 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 CronPicker from './CronPicker.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { getDictItemsByCode } from '@/api/dict';
import { useDraggable } from '@/utils/drag';
interface Props {
visible: boolean;
initialValues?: any | null;
loading?: boolean;
}
const categoryCodeOption:any =ref([])
const localLoading = ref(false);
const props = withDefaults(defineProps<Props>(), {
visible: false,
@ -309,368 +135,82 @@ const formRef = ref();
const defaultFormData = reactive({
id: undefined,
name: undefined,
code: undefined,
rvnm: undefined,
description: undefined
messageType: undefined,
categoryCode: undefined,
categoryName: undefined,
baseIdList: [],
baseNameList: [],
cronExpression: undefined
});
const formData: any = reactive({ ...defaultFormData });
const JidiSelectEventStore = useJidiSelectEventStore(); //jidiData
const baseIdsOption = computed(() => {
const hasAll = (formData.baseIdList || []).includes('all');
return JidiSelectEventStore.jidiData.map(item => ({
value: item.wbsCode,
label: item.wbsName,
disabled: hasAll && item.wbsCode !== 'all'
}));
});
const handleBaseChange = (value: string[]) => {
if ((value || []).includes('all')) {
formData.baseIdList = ['all'];
formData.baseNameList = ['当前全部'];
} else {
formData.baseNameList = (value || []).map(code => {
const item = JidiSelectEventStore.jidiData.find(d => d.wbsCode === code);
return item?.wbsName || '';
});
}
};
const handleCategoryChange = (value: string) => {
const option = categoryCodeOption.value.find(o => o.value === value);
formData.categoryName = option?.label || '';
};
const rules: Record<string, Rule[]> = {
name: [{ required: true, message: '请输入沿程配置名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入配置编码', trigger: 'blur' }],
rvnm: [{ required: true, message: '请输入所在河段', trigger: 'blur' }]
messageType: [{ required: true, message: '请输入类型名称', trigger: 'blur' }],
categoryCode: [{ required: true, message: '请选择所属类别', trigger: 'blur' }]
};
const isEdit = computed(() => !!props.initialValues);
//
const treeData = ref<any[]>([]);
//
const addingNewNode = ref(false);
const newNodeData = ref({
stcd: undefined,
stnm: '',
sttpName: ''
});
//
const addingChildNode = ref(false);
const currentParentKey = ref<string | null>(null);
const childNodeData = ref({
stcd: undefined,
stnm: '',
sttpName: ''
});
//
const editingNode = ref(false);
const editingNodeKey = ref<string | null>(null);
const editNodeData = ref({
stcd: undefined,
stnm: '',
sttpName: '',
originalData: null as any
});
//
const stationList = ref([
{ stcd: '1', stnm: '站点A', sttpName: '类型1' },
{ stcd: '2', stnm: '站点B', sttpName: '类型2' },
{ stcd: '3', stnm: '站点C', sttpName: '类型1' },
{ stcd: '4', stnm: '站点D', sttpName: '类型3' }
]);
// ID
let nodeIdCounter = 0;
const generateNodeId = () => `node_${Date.now()}_${++nodeIdCounter}`;
//
const getNodeTitle = (dataRef: any) => {
if (!dataRef.parentId) {
// stnm
return dataRef.stnm || '';
} else {
// stnm + sttpName
return `${dataRef.stnm || ''} ${dataRef.sttpName || ''}`;
}
const cronModalVisible = ref(false);
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
useDraggable(cronModalVisible, { boundary: true, resetOnOpen: true });
const openCronModal = () => {
cronModalVisible.value = true;
};
//
const filterOption = (input: string, option: any) => {
return (
option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
);
const closeCronModal = () => {
cronModalVisible.value = false;
};
//
const isFirstNode = (dataRef: any) => {
const siblings = getSiblings(dataRef);
return siblings.length > 0 && siblings[0].key === dataRef.key;
const handleCronConfirm = (value: string) => {
formData.cronExpression = value;
closeCronModal();
};
//
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 = () => {
addingNewNode.value = true;
newNodeData.value = {
stcd: undefined,
stnm: '',
sttpName: ''
};
};
//
const handleNodeSelect = (value: string) => {
const station = stationList.value.find(item => item.stcd === value);
if (station) {
newNodeData.value.stnm = station.stnm;
newNodeData.value.sttpName = station.sttpName;
}
};
//
const handleSaveNewNode = () => {
if (!newNodeData.value.stcd) {
message.error('请选择站点');
return;
}
const newNode = {
key: generateNodeId(),
stcd: newNodeData.value.stcd,
stnm: newNodeData.value.stnm,
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,
stnm: '',
sttpName: ''
};
};
//
const handleAddChild = (parentData: any) => {
addingChildNode.value = true;
currentParentKey.value = parentData.key;
childNodeData.value = {
stcd: undefined,
stnm: '',
sttpName: ''
};
};
//
const handleChildNodeSelect = (value: string) => {
const station = stationList.value.find(item => item.stcd === value);
if (station) {
childNodeData.value.stnm = 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,
stnm: childNodeData.value.stnm,
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,
stnm: '',
sttpName: ''
};
};
//
const handleEditNode = (dataRef: any) => {
editingNode.value = true;
editingNodeKey.value = dataRef.key;
//
editNodeData.value = {
stcd: dataRef.stcd,
stnm: dataRef.stnm,
sttpName: dataRef.sttpName,
originalData: { ...dataRef }
};
};
//
const handleEditNodeSelect = (value: string) => {
const station = stationList.value.find(item => item.stcd === value);
if (station) {
editNodeData.value.stnm = 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.stnm = editNodeData.value.stnm;
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,
stnm: '',
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.stnm}"吗?`,
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);
const submitData: any = { ...formData };
if ((formData.baseIdList || []).includes('all')) {
const allCodes = JidiSelectEventStore.jidiData
.filter(item => item.wbsCode !== 'all')
.map(item => item.wbsCode);
const allNames = JidiSelectEventStore.jidiData
.filter(item => item.wbsCode !== 'all')
.map(item => item.wbsName);
submitData.baseIdList = allCodes.join(',');
submitData.baseNameList = allNames.join(',');
} else {
submitData.baseIdList = (formData.baseIdList || []).join(',');
submitData.baseNameList = (formData.baseNameList || []).join(',');
}
emit('ok', submitData);
} catch (error) {
console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确');
@ -678,32 +218,46 @@ const handleOk = async () => {
};
const initForm = () => {
resetForm();
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 if (key === 'baseIdList' || key === 'baseNameList') {
formData[key] = 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
})) || []
}));
if (values.baseIdList) {
const baseIds = typeof values.baseIdList === 'string'
? values.baseIdList.split(',')
: (Array.isArray(values.baseIdList) ? values.baseIdList : []);
const allBaseCodes = JidiSelectEventStore.jidiData
.filter(item => item.wbsCode !== 'all')
.map(item => item.wbsCode);
const isAllSelected = allBaseCodes.every(code => baseIds.includes(code));
if (isAllSelected) {
formData.baseIdList = ['all'];
formData.baseNameList = ['当前全部'];
} else {
treeData.value = [];
formData.baseIdList = baseIds;
formData.baseNameList = baseIds.map((code: string) => {
const item = JidiSelectEventStore.jidiData.find(d => d.wbsCode === code);
return item?.wbsName || '';
});
}
}
if (values.categoryCode) {
const option = categoryCodeOption.value.find(o => o.value === values.categoryCode);
formData.categoryName = option?.label || '';
}
} else {
resetForm();
}
};
@ -712,8 +266,6 @@ const resetForm = () => {
formRef.value.resetFields();
}
Object.assign(formData, defaultFormData);
treeData.value = [];
addingNewNode.value = false;
};
const handleCancel = () => {
@ -732,6 +284,17 @@ watch(
{ immediate: false }
);
const initOption = async () => {
let res = await getDictItemsByCode({dictCode: 'category'});
categoryCodeOption.value = res.data.map(item => ({
value: item.itemCode,
label: item.dictName
}));
};
onMounted(() => {
initOption();
});
defineExpose({
localLoading
});

View File

@ -4,6 +4,7 @@
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
:showSearch="false"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
@ -38,22 +39,22 @@ const initSearchData = {
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "Input",
name: "name",
label: "沿程配置名称",
fieldProps: {
allowClear: true,
},
},
{
type: "Input",
name: "rvnm",
label: "所在河段",
fieldProps: {
allowClear: true,
},
},
// {
// type: "Input",
// name: "name",
// label: "沿",
// fieldProps: {
// allowClear: true,
// },
// },
// {
// type: "Input",
// name: "rvnm",
// label: "",
// fieldProps: {
// allowClear: true,
// },
// },
]);
const onSearchFinish = (values: any) => {

View File

@ -0,0 +1,707 @@
<template>
<div class="cron-picker">
<a-tabs v-model:activeKey="activeField" type="card" :centered="true">
<a-tab-pane v-for="field in fields" :key="field.key" :tab="field.label">
<div class="field-content">
<a-radio-group
v-model:value="fieldData[field.key].type"
button-style="outline"
size="small"
>
<a-space direction="vertical" :size="12">
<a-radio v-if="field.key !== 'year'" value="all">
<span class="option-label"
>{{ field.allLabel }} 允许的通配符[,-*]</span
>
</a-radio>
<a-radio v-if="field.key === 'year'" value="all">
<span class="option-label">不指定 允许的通配符[,-*]</span>
</a-radio>
<a-radio
v-if="
field.key === 'day' ||
field.key === 'month' ||
field.key === 'week'
"
value="none"
>
<span class="option-label">不指定</span>
</a-radio>
<a-radio v-if="field.key === 'year'" value="yearly">
<span class="option-label">每年</span>
</a-radio>
<a-radio
v-if="field.key !== 'day' && field.key !== 'year'"
value="range"
>
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].rangeStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label"></span>
<a-input-number
v-model:value="fieldData[field.key].rangeEnd"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span v-if="field.key !== 'week'" class="option-label">{{
field.unit
}}</span>
<span v-else class="option-label">到星期</span>
</a-radio>
<a-radio v-if="field.key === 'year'" value="range">
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].rangeStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label"></span>
<a-input-number
v-model:value="fieldData[field.key].rangeEnd"
:min="field.min"
:max="field.max"
class="range-input"
/>
</a-radio>
<a-radio
v-if="field.key !== 'day' && field.key !== 'year'"
value="interval"
>
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label">{{ field.unit }}开始</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStep"
:min="1"
:max="field.max"
class="range-input"
/>
<span class="option-label">{{ field.unit }}执行一次</span>
</a-radio>
<a-radio v-if="field.key === 'day'" value="interval">
<span class="option-label">周期从</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStart"
:min="field.min"
:max="field.max"
class="range-input"
/>
<span class="option-label">日开始</span>
<a-input-number
v-model:value="fieldData[field.key].intervalStep"
:min="1"
:max="field.max"
class="range-input"
/>
<span class="option-label">日执行一次</span>
</a-radio>
<a-radio v-if="field.key === 'week'" value="nthWeek">
<span class="option-label"></span>
<a-input-number
v-model:value="fieldData[field.key].nthWeek"
:min="1"
:max="5"
class="range-input"
/>
<span class="option-label">周的星期</span>
<a-input-number
v-model:value="fieldData[field.key].nthWeekDay"
:min="1"
:max="7"
class="range-input"
/>
</a-radio>
<a-radio v-if="field.key === 'week'" value="lastWeekDay">
<span class="option-label">本月最后一个星期</span>
<a-input-number
v-model:value="fieldData[field.key].lastWeekDay"
:min="1"
:max="7"
class="range-input"
/>
<span class="option-label">(周w)</span>
</a-radio>
<a-radio v-if="field.key === 'day'" value="weekday">
<span class="option-label">每月</span>
<a-input-number
v-model:value="fieldData[field.key].weekdayDay"
:min="1"
:max="31"
class="range-input"
/>
<span class="option-label">号最近的那个工作日</span>
</a-radio>
<a-radio v-if="field.key === 'day'" value="lastDay">
<span class="option-label">本月最后一天</span>
</a-radio>
<a-radio
v-if="field.key !== 'year'"
value="specify"
class="zhding"
>
<span class="option-label" style="min-width: 30px;">指定</span>
<div v-if="field.key === 'hour'" class="hour-grid">
<a-checkbox-group
v-model:value="fieldData[field.key].specifyValues"
>
<div>
<span class="grid-label">上午:</span>
<a-space wrap class="checkbox-grid">
<a-checkbox
v-for="val in getHourValues(0, 11)"
:key="val"
:value="val.toString()"
>
{{ formatHour(val) }}
</a-checkbox>
</a-space>
</div>
<div>
<span class="grid-label">下午:</span>
<a-space wrap class="checkbox-grid">
<a-checkbox
v-for="val in getHourValues(12, 23)"
:key="val"
:value="val.toString()"
>
{{ formatHour(val) }}
</a-checkbox>
</a-space>
</div>
</a-checkbox-group>
</div>
<div v-else class="specify-grid">
<a-checkbox-group
v-model:value="fieldData[field.key].specifyValues"
>
<a-space wrap class="checkbox-grid">
<a-checkbox
v-for="val in getFieldValues(field)"
:key="val"
:value="val.toString()"
>
{{
formatValue(
val,
field.key === 'second' ||
field.key === 'minute' ||
field.key === 'hour'
)
}}
</a-checkbox>
</a-space>
</a-checkbox-group>
</div>
</a-radio>
</a-space>
</a-radio-group>
</div>
</a-tab-pane>
</a-tabs>
<div class="cron-preview">
<a-alert type="info" :message="`Cron表达式: ${currentCron}`" showIcon />
</div>
<div class="cron-actions">
<a-button @click="handleReset">重置</a-button>
<a-button type="primary" @click="handleConfirm">确认选择</a-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue';
interface Props {
modelValue?: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'confirm', value: string): void;
}>();
const fields = [
{ key: 'second', label: '秒', min: 0, max: 59, unit: '秒', allLabel: '每秒' },
{
key: 'minute',
label: '分钟',
min: 0,
max: 59,
unit: '分钟',
allLabel: '每分钟'
},
{
key: 'hour',
label: '小时',
min: 0,
max: 23,
unit: '小时',
allLabel: '每小时'
},
{ key: 'day', label: '日', min: 1, max: 31, unit: '日', allLabel: '每日' },
{ key: 'month', label: '月', min: 1, max: 12, unit: '月', allLabel: '每月' },
{ key: 'week', label: '周', min: 1, max: 7, unit: '周', allLabel: '每周' },
{
key: 'year',
label: '年',
min: 1970,
max: 2099,
unit: '年',
allLabel: '每年'
}
];
const activeField = ref('second');
const fieldData = reactive({
second: {
type: 'all',
rangeStart: 0,
rangeEnd: 59,
intervalStart: 0,
intervalStep: 1,
specifyValues: []
},
minute: {
type: 'all',
rangeStart: 0,
rangeEnd: 59,
intervalStart: 0,
intervalStep: 1,
specifyValues: []
},
hour: {
type: 'all',
rangeStart: 0,
rangeEnd: 23,
intervalStart: 0,
intervalStep: 1,
specifyValues: []
},
day: {
type: 'all',
rangeStart: 1,
rangeEnd: 31,
intervalStart: 1,
intervalStep: 1,
weekdayDay: 1,
specifyValues: []
},
month: {
type: 'all',
rangeStart: 1,
rangeEnd: 12,
intervalStart: 1,
intervalStep: 1,
specifyValues: []
},
week: {
type: 'all',
rangeStart: 1,
rangeEnd: 7,
intervalStart: 1,
intervalStep: 1,
nthWeek: 1,
nthWeekDay: 1,
lastWeekDay: 1,
specifyValues: []
},
year: {
type: 'all',
rangeStart: 1970,
rangeEnd: 2099,
intervalStart: 1970,
intervalStep: 1,
specifyValues: []
}
});
const getFieldValues = (field: any) => {
const values: number[] = [];
for (let i = field.min; i <= field.max; i++) {
values.push(i);
}
return values;
};
const getHourValues = (start: number, end: number) => {
const values: number[] = [];
for (let i = start; i <= end; i++) {
values.push(i);
}
return values;
};
const formatValue = (val: number, pad: boolean) => {
return pad ? val.toString().padStart(2, '0') : val.toString();
};
const formatHour = (val: number) => {
return val.toString().padStart(2, '0');
};
const isDaySpecific = () => {
const day = fieldData.day;
return day.type !== 'all' && day.type !== 'lastDay';
};
const isWeekSpecific = () => {
const week = fieldData.week;
return week.type !== 'all' && week.type !== 'none';
};
const generateFieldValue = (key: string) => {
const data = fieldData[key];
switch (data.type) {
case 'all':
if (key === 'day' && isWeekSpecific()) return '?';
if (key === 'week' && isDaySpecific()) return '?';
if (key === 'year') return '';
return '*';
case 'none':
return '?';
case 'yearly':
return '*';
case 'range':
return `${data.rangeStart}-${data.rangeEnd}`;
case 'interval':
return `${data.intervalStart}/${data.intervalStep}`;
case 'nthWeek':
return `${data.nthWeek}#${data.nthWeekDay}`;
case 'lastWeekDay':
return `L#${data.lastWeekDay}`;
case 'weekday':
return `W${data.weekdayDay}`;
case 'lastDay':
return 'L';
case 'specify':
if (data.specifyValues.length === 0) {
if (key === 'day' && isWeekSpecific()) return '?';
if (key === 'week' && isDaySpecific()) return '?';
return '*';
}
return data.specifyValues
.sort((a: string, b: string) => parseInt(a) - parseInt(b))
.join(',');
default:
return '*';
}
};
const currentCron = computed(() => {
const values = [
generateFieldValue('second'),
generateFieldValue('minute'),
generateFieldValue('hour'),
generateFieldValue('day'),
generateFieldValue('month'),
generateFieldValue('week')
];
const yearValue = generateFieldValue('year');
if (yearValue) {
values.push(yearValue);
}
return values.join(' ');
});
const validateCron = (
expression: string
): { valid: boolean; errorMessage: string } => {
const parts = expression.split(' ');
if (parts.length !== 6 && parts.length !== 7) {
return { valid: false, errorMessage: '表达式格式错误应为6或7个字段' };
}
const [second, minute, hour, day, month, week, year] = parts;
const isNumber = (val: string) => /^\d+$/.test(val);
const isWildcard = (val: string) => val === '*' || val === '?';
const isRange = (val: string) => /^\d+-\d+$/.test(val);
const isInterval = (val: string) => /^\d+\/\d+$/.test(val);
const isList = (val: string) => /^(\d+,)+\d+$/.test(val);
const validateRange = (
val: string,
min: number,
max: number,
label: string
) => {
if (isWildcard(val)) return true;
if (isNumber(val)) {
const num = parseInt(val);
return num >= min && num <= max;
}
if (isRange(val)) {
const [start, end] = val.split('-').map(Number);
return start >= min && end <= max && start <= end;
}
if (isInterval(val)) {
const [start, step] = val.split('/').map(Number);
return start >= min && start <= max && step >= 1;
}
if (isList(val)) {
return val.split(',').every(v => {
const num = parseInt(v);
return num >= min && num <= max;
});
}
return false;
};
if (!validateRange(second, 0, 59, '秒')) {
return { valid: false, errorMessage: '秒字段值无效范围应为0-59' };
}
if (!validateRange(minute, 0, 59, '分钟')) {
return { valid: false, errorMessage: '分钟字段值无效范围应为0-59' };
}
if (!validateRange(hour, 0, 23, '小时')) {
return { valid: false, errorMessage: '小时字段值无效范围应为0-23' };
}
if (
!isWildcard(day) &&
!isRange(day) &&
!isInterval(day) &&
!isList(day) &&
!day.startsWith('W') &&
day !== 'L'
) {
const num = parseInt(day);
if (isNaN(num) || num < 1 || num > 31) {
return { valid: false, errorMessage: '日字段值无效范围应为1-31' };
}
}
if (!validateRange(month, 1, 12, '月')) {
return { valid: false, errorMessage: '月字段值无效范围应为1-12' };
}
if (
!isWildcard(week) &&
!isRange(week) &&
!isList(week) &&
!week.includes('#') &&
!week.startsWith('L')
) {
const num = parseInt(week);
if (isNaN(num) || num < 1 || num > 7) {
return { valid: false, errorMessage: '周字段值无效范围应为1-7' };
}
}
if (year && !validateRange(year, 1970, 2099, '年')) {
return { valid: false, errorMessage: '年字段值无效范围应为1970-2099' };
}
if (day !== '?' && week !== '?') {
return {
valid: false,
errorMessage: '日和周不能同时指定,请将其中一个设为"不指定"'
};
}
return { valid: true, errorMessage: '' };
};
const handleConfirm = () => {
const { valid, errorMessage } = validateCron(currentCron.value);
if (!valid) {
message.error(errorMessage);
return;
}
emit('confirm', currentCron.value);
};
const handleReset = () => {
Object.keys(fieldData).forEach(key => {
const field = fields.find(f => f.key === key);
if (field) {
fieldData[key as keyof typeof fieldData] = {
type: key === 'year' ? 'all' : 'all',
rangeStart: field.min,
rangeEnd: field.max,
intervalStart: field.min,
intervalStep: 1,
weekdayDay: 1,
nthWeek: 1,
nthWeekDay: 1,
lastWeekDay: 1,
specifyValues: []
};
}
});
activeField.value = 'second';
};
const parseCron = (expression: string) => {
const parts = expression.split(' ');
const keys = ['second', 'minute', 'hour', 'day', 'month', 'week'];
keys.forEach((key, index) => {
const value = parts[index] || '*';
const data = fieldData[key as keyof typeof fieldData];
if (value === '*') {
data.type = 'all';
} else if (value === '?') {
data.type = 'none';
} else if (value.includes('-')) {
data.type = 'range';
const [start, end] = value.split('-').map(Number);
data.rangeStart = start;
data.rangeEnd = end;
} else if (value.includes('/')) {
data.type = 'interval';
const [start, step] = value.split('/').map(Number);
data.intervalStart = start;
data.intervalStep = step;
} else if (value.includes('#')) {
data.type = 'nthWeek';
const [nth, day] = value.split('#').map(Number);
data.nthWeek = nth;
data.nthWeekDay = day;
} else if (value.startsWith('L#')) {
data.type = 'lastWeekDay';
data.lastWeekDay = parseInt(value.substring(2));
} else if (value.startsWith('W')) {
data.type = 'weekday';
data.weekdayDay = parseInt(value.substring(1));
} else if (value === 'L') {
data.type = 'lastDay';
} else if (value.includes(',')) {
data.type = 'specify';
data.specifyValues = value.split(',');
} else {
data.type = 'specify';
data.specifyValues = [value];
}
});
const yearData = fieldData.year;
if (parts.length === 7) {
const yearValue = parts[6];
if (yearValue === '*') {
yearData.type = 'yearly';
} else if (yearValue.includes('-')) {
yearData.type = 'range';
const [start, end] = yearValue.split('-').map(Number);
yearData.rangeStart = start;
yearData.rangeEnd = end;
} else {
yearData.type = 'specify';
yearData.specifyValues = yearValue.includes(',')
? yearValue.split(',')
: [yearValue];
}
} else {
yearData.type = 'all';
}
};
watch(
() => props.modelValue,
val => {
if (val && val.trim()) {
parseCron(val.trim());
} else if (val === undefined || val === null) {
handleReset();
}
},
{ immediate: true }
);
</script>
<style scoped lang="scss">
.cron-picker {
padding: 8px;
}
.field-content {
padding: 16px 0;
}
.option-label {
margin-right: 4px;
}
.range-input {
width: 60px;
margin: 0 4px;
}
.checkbox-grid {
max-width: 100%;
}
.hour-grid {
display: flex;
flex-direction: column;
gap: 8px;
// margin-top: 8px;
.grid-label {
font-size: 14px;
color: #000;
display: inline-block;
margin: 0px 10px 0px 5px;
}
}
.specify-grid {
// margin-top: 8px;
}
.cron-preview {
margin-top: 16px;
}
.cron-actions {
margin-top: 16px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
:deep(.ant-radio-wrapper span.ant-radio + *) {
display: flex;
align-items: center;
}
.zhding {
:deep(span.ant-radio + *) {
display: flex !important;
align-items: flex-start !important;
// margin-top: px;
}
:deep(.ant-radio){
align-self:auto !important;
// margin-top: 0px !important;
}
}
</style>

View File

@ -0,0 +1,198 @@
<template>
<a-modal
:open="visible"
title="推送目标配置"
:width="780"
:footer="null"
@cancel="handleCancel"
>
<div class="push-target-modal">
<a-radio-group
v-model:value="targetType"
:options="targetOptions"
class="target-radio"
/>
<div v-show="targetType !== '1'" class="transfer-wrapper">
<a-transfer
v-model:target-keys="targetKeys"
v-model:selected-keys="selectedKeys"
:data-source="transferDataSource"
:titles="transferTitles"
:show-search="true"
:filter-option="filterOption"
:render="item => item.title"
:list-style="{ width: '340px', height: '360px' }"
@change="handleChange"
/>
</div>
<div class="modal-footer">
<a-button type="primary" @click="handleSave" :loading="loading">保存</a-button>
<a-button @click="handleCancel">放弃</a-button>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue';
import {queryUsersList,queryRolesList,getPushTargets} from '@/api/system/map/pushManagement';
import { useDraggable } from '@/utils/drag';
const props = defineProps<{
visible: boolean;
initialValues: any;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'cancel'): void;
(
e: 'ok',
values: {
targetType: string;
userRoleIds: string[] | null;
configId?: number;
}
): void;
}>();
const targetOptions = [
{ label: '所有人', value: '1' },
{ label: '按人', value: '2' },
{ label: '按角色类型', value: '3' }
];
const targetType = ref('1');
const selectedKeys = ref<string[]>([]);
const targetKeys = ref<string[]>([]);
const userList = ref<{ key: string; title: string }[]>([]);
const roleList = ref<{ key: string; title: string }[]>([]);
const loading = ref(false);
const isInitializing = ref(false);
const transferReady = ref(false);
const cachedPushTargets = ref<any>(null);
const transferDataSource = computed(() => {
return targetType.value === '2' ? userList.value : roleList.value;
});
const transferTitles = computed(() => {
if (targetType.value === '2') {
return ['未关联用户', '已关联用户'];
}
return ['未关联角色', '已关联角色'];
});
const filterOption = (inputValue: string, item: any) => {
return item.title.toLowerCase().includes(inputValue.toLowerCase());
};
const handleEcho = async (data: any) => {
if (!data) return;
const newTargetType = String(data.targetType ?? '1');
const newUserRoleIds = Array.isArray(data.userRoleIds) ? data.userRoleIds : [];
targetType.value = newTargetType;
if (newTargetType !== '1' && newUserRoleIds.length > 0) {
await nextTick();
const dataSource = newTargetType === '2' ? userList.value : roleList.value;
const validKeys = newUserRoleIds.filter(id =>
dataSource.some(item => item.key === id)
);
targetKeys.value = [...validKeys];
} else {
targetKeys.value = [];
}
};
watch(
() => props.visible,
async val => {
if (val) {
loading.value = true;
targetType.value = '1';
targetKeys.value = [];
selectedKeys.value = [];
await initOption();
if (props.initialValues?.id) {
try {
const resh = await getPushTargets({ configId: props.initialValues.id });
if (resh?.data) {
await handleEcho(resh.data);
}
} catch (error) {
console.error('获取推送目标失败:', error);
}
}
loading.value = false;
}
}
);
const handleChange = (newTargetKeys: string[]) => {
targetKeys.value = newTargetKeys;
};
const handleSave = () => {
let userRoleIds: string[] | null = null;
if (targetType.value === '1') {
userRoleIds = userList.value.map(item => item.key);
} else {
userRoleIds = targetKeys.value as string[];
}
emit('ok', {
targetType: targetType.value,
userRoleIds,
configId: props.initialValues?.id
});
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
};
const initOption = async () => {
let res = await queryUsersList({});
let resjv = await queryRolesList({});
userList.value = (res.data || []).map((item: any) => ({
key: item.id,
title: `${item.nickname || item.username || ''}${item.phone ? '(' + item.phone + ')' : ''}`
}));
roleList.value = (resjv.data || []).map((item: any) => ({
key: item.id,
title: item.rolename || ''
}));
};
onMounted(() => {});
</script>
<style scoped lang="scss">
.push-target-modal {
display: flex;
flex-direction: column;
gap: 20px;
}
.target-radio {
margin-bottom: 4px;
}
.transfer-wrapper {
display: flex;
justify-content: center;
}
.modal-footer {
display: flex;
gap: 12px;
justify-content: flex-start;
}
</style>

View File

@ -7,15 +7,31 @@
:handle-add="handleAdd"
@reset="handleReset"
@search-finish="onSearchFinish"
/>
/> -->
<div><a-button type="primary" @click="handleAdd">新增</a-button></div>
<BasicTable
ref="basicTable"
:columns="columns"
:list-url="getAllConfigTree"
:list-url="getPushConfigList"
:search-params="searchParams"
>
<template #isSms="{ column, record }">
<a-switch
v-model:checked="record.isSms"
:unCheckedValue="0"
:checkedValue="1"
@change="switchChange(record.isSms, record)"
/>
</template>
<template #action="{ column, record }">
<div class="flex gap-[6px]">
<a-button
class="!p-0"
type="link"
size="small"
@click="handlePushTarget(record)"
>推送目标</a-button
>
<a-button
class="!p-0"
type="link"
@ -40,79 +56,61 @@
:initial-values="currentRecord"
@cancel="editModalCancel"
@ok="handleEditSubmit"
/> -->
/>
<PushTargetModal
ref="pushTargetModal"
v-model:visible="pushTargetModalVisible"
:initial-values="pushTargetInitialValues"
@cancel="pushTargetModalCancel"
@ok="handlePushTargetSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue';
import { ref, nextTick, computed,onMounted } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import ConfigManagementSearch from './ConfigManagementSearch.vue';
import ConfigManagementForm from './ConfigManagementForm.vue';
import PushTargetModal from './PushTargetModal.vue';
import { message, Modal } from 'ant-design-vue';
import {
getAllConfigTree,
deleteBaseWbsb,
saveBaseWbsb
} from '@/api/system/map/ConfigManagement';
getPushConfigList,
addPushConfig,
updatePushConfig,
deletePushConfig,
savePushTargets,
setPushConfigSmsSwitch
} from '@/api/system/map/pushManagement';
//
const basicTable = ref<any>(null);
//
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 60,
align: 'center',
customRender: ({ text, record, index }) => index + 1
title: '消息类型',
dataIndex: 'messageType',
key: 'messageType'
},
{
title: '沿程配置名称',
dataIndex: 'name',
key: 'name',
width: 150
title: '当日推送次数',
key: 'dailyPushCount',
dataIndex: 'dailyPushCount'
},
{
title: '配置编码',
key: 'code',
dataIndex: 'code',
width: 120
title: '累计推送次数',
dataIndex: 'pushCount',
key: 'pushCount'
},
{
title: '所在河段',
dataIndex: 'rvnm',
key: 'rvnm',
ellipsis: true
},
{
title: '创建人',
key: 'recordUser',
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: 'isSms',
dataIndex: 'isSms',
slots: { customRender: 'isSms' }
},
{
title: '操作',
key: 'action',
width: 120,
width: 160,
fixed: 'right',
slots: { customRender: 'action' }
}
@ -124,6 +122,11 @@ const searchParams = ref({});
const currentRecord = ref<any | null>(null);
const editModalVisible = ref(false);
//
const pushTargetModal = ref<any>(null);
const pushTargetModalVisible = ref(false);
const pushTargetInitialValues = ref<any | null>(null);
//
const handleAdd = () => {
currentRecord.value = null;
@ -136,6 +139,33 @@ const handleEdit = (record: any) => {
editModalVisible.value = true;
};
//
const handlePushTarget = (record: any) => {
pushTargetInitialValues.value = { ...record };
pushTargetModalVisible.value = true;
};
//
const pushTargetModalCancel = () => {
pushTargetInitialValues.value = null;
pushTargetModalVisible.value = false;
};
//
const handlePushTargetSubmit = async (values: any) => {
try {
let res: any = await savePushTargets(values);
if (res.code == 0) {
message.success('保存成功');
}
pushTargetModalVisible.value = false;
basicTable.value.refresh();
} catch (error) {
// message.error('');
return;
}
};
//
const handleDelete = (record: any) => {
Modal.confirm({
@ -144,11 +174,13 @@ const handleDelete = (record: any) => {
zIndex: 2002,
onOk: async () => {
try {
let res = await deleteBaseWbsb([record.id]);
let res:any = await deletePushConfig([record.id]);
if ((res.code = 0)) {
message.success('删除成功');
}
basicTable.value.refresh();
} catch (error) {
message.error('删除失败');
// message.error('');
return;
}
}
@ -156,39 +188,13 @@ const handleDelete = (record: any) => {
};
//
const onSearchFinish = (values: any) => {
const onSearchFinish = () => {
const params = {
logic: 'and',
filters: [
values.parentId
? {
field: 'parentId',
operator: 'eq',
dataType: 'string',
value: values.parentId
}
: null,
values.name
? {
field: 'name',
operator: 'contains',
dataType: 'string',
value: values.name
}
: null
].filter(Boolean)
filters: [].filter(Boolean)
};
basicTable.value.getList(params);
};
//
const handleReset = () => {
basicTable.value.getList({
logic: 'and',
filters: []
});
};
//
const editModalCancel = () => {
currentRecord.value = null;
@ -198,25 +204,55 @@ const editModalCancel = () => {
//
const handleEditSubmit = async (values: any) => {
try {
let res = await saveBaseWbsb({
...currentRecord.value,
...values
});
//updatePushConfig
if (values.id) {
let res: any = await updatePushConfig({ ...values });
if ((res.code = 0)) {
message.success(`修改成功`);
}
} else {
let res: any = await addPushConfig({ ...values });
if ((res.code = 0)) {
message.success(`保存成功`);
}
}
editModalVisible.value = false;
basicTable.value.refresh();
} catch (error) {
message.error(`保存失败`);
// message.error(``);
editModalVisible.value = false;
return;
}
};
//
const switchChange = async (checked: any, record: any) => {
Modal.confirm({
title: '确认修改',
content: '确定要修改短信提醒的开关状态吗?',
zIndex: 2002,
onOk: async () => {
try {
let res: any = await setPushConfigSmsSwitch({
id: record.id,
isSms: checked ? '1' : '0'
});
if (res.code == 0) {
message.success(`修改成功`);
basicTable.value.refresh();
}
} catch (error) {
return;
}
}
});
};
const initOption = () => {};
const initOption = async () => {};
onMounted(() => {
initOption();
onSearchFinish();
});
</script>

View File

@ -0,0 +1,206 @@
<template>
<a-modal
v-model:open="visible"
title="推送情况"
:footer="null"
width="1000px"
>
<a-tabs
v-if="resultStatusOption.length > 0"
v-model:activeKey="activeTab"
class="mb-4"
>
<a-tab-pane
v-for="item in resultStatusOption"
:key="item.value"
:tab="item.label"
></a-tab-pane>
</a-tabs>
<a-form layout="inline" class="mb-4">
<a-form-item>
<a-input
v-model:value="searchTarget"
placeholder="请输入推送目标"
style="width: 200px"
allow-clear
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
</a-form-item>
</a-form>
<BasicTable
ref="basicTable"
:columns="columns"
:list-url="getPushHistoryDetailList"
:search-params="searchParams"
:scroll-y="500"
>
<template #status="{ record }">
<span
:class="
record.status == '1'
? 'text-success'
: record.status == '2'
? 'text-error'
: ''
"
>
{{
record.status == '1'
? '推送成功'
: record.status == '2'
? '推送失败'
: '待推送'
}}
</span>
</template>
</BasicTable>
</a-modal>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick } from 'vue';
import {
getPushHistoryDetailList,
getPushHistoryDetailById
} from '@/api/system/map/pushManagement';
import { getDictItemsByCode } from '@/api/dict';
import BasicTable from '@/components/BasicTable/index.vue';
import { useDraggable } from '@/utils/drag';
const props = defineProps<{
visible: boolean;
record: any;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const visible = computed({
get: () => props.visible,
set: value => emit('update:visible', value)
});
useDraggable(visible, { boundary: true, resetOnOpen: true });
const searchTarget = ref('');
const activeTab = ref('all');
//
const basicTable = ref<any>(null);
const searchParams = ref({
sort: [
{
field: 'createdAt',
dir: 'desc'
}
]
});
const columns = [
{ title: '推送目标', dataIndex: 'targetName', key: 'targetName' },
{ title: '推送方式', dataIndex: 'sendChannel', key: 'sendChannel' },
{ title: '推送时间', dataIndex: 'sendTime', key: 'sendTime' },
{ title: '推送结果', key: 'status', slots: { customRender: 'status' } }
];
const resultStatusOption = ref([]);
const handleSearch = () => {
if (!basicTable.value || !props.record || !props.record.id) {
return;
}
const filters = [];
filters.push({
field: 'historyId',
operator: 'eq',
dataType: 'string',
value: props.record.id
});
if (activeTab.value !== 'all') {
filters.push({
field: 'status',
operator: 'eq',
dataType: 'string',
value: activeTab.value
});
}
if (searchTarget.value) {
filters.push({
field: 'targetName',
operator: 'contains',
dataType: 'string',
value: searchTarget.value
});
}
const params = {
logic: 'and',
filters
};
basicTable.value.getList(params);
};
const getDict = async () => {
try {
let res = await getDictItemsByCode({ dictCode: 'result_status' });
if (res.data.length > 0) {
resultStatusOption.value = res.data.map((item: any) => ({
value: item.itemCode,
label: item.dictName
}));
resultStatusOption.value.unshift({ value: 'all', label: '全部' });
}
} catch (error) {
console.error('获取字典失败:', error);
}
};
onMounted(async () => {
await getDict();
});
watch(activeTab, () => {
handleSearch();
});
watch(
() => props.record,
async newVal => {
if (newVal && props.visible) {
await nextTick();
await nextTick();
if (basicTable.value) {
handleSearch();
}
let res1 = await getPushHistoryDetailById({ historyId: props.record.id });
const {
failCount = 0,
pendingCount = 0,
successCount = 0
} = res1.data || {};
const total = failCount + pendingCount + successCount;
resultStatusOption.value = resultStatusOption.value.map(item => {
let count = 0;
if (item.value === 'all') count = total;
else if (item.value === '1') count = successCount;
else if (item.value === '2') count = failCount;
else if (item.value === '0') count = pendingCount;
const originalLabel = item.originalLabel || item.label;
return {
...item,
originalLabel,
label: `${originalLabel}(${count})`
};
});
}
},
{ deep: true }
);
defineExpose({
handleSearch
});
</script>
<style scoped lang="scss">
.text-success {
color: #52c41a;
}
.text-error {
color: #ff4d4f;
}
</style>

View File

@ -1,208 +1,161 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\LayerManagement\index.vue -->
<template>
<div class="content">
推送历史
<!-- <LayerManagementSearch
ref="layerManagementSearch"
:handle-add="handleAdd"
@reset="handleReset"
@search-finish="onSearchFinish"
<!-- 推送历史 -->
<a-form layout="inline" class="search-form">
<a-form-item label="">
<a-input
v-model:value="searchForm.title"
placeholder="请输入标题关键字"
allow-clear
style="width: 180px"
@pressEnter="onSearchFinish()"
/>
</a-form-item>
<a-form-item label="起止时间">
<a-range-picker
v-model:value="searchForm.planTime"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="onSearchFinish()"
/>
</a-form-item>
<a-form-item label="">
<a-select
v-model:value="searchForm.messageTypeCode"
placeholder="请选择消息类型"
style="width: 180px"
allow-clear
show-search
:filter-option="filterOption"
:options="messageTypeCodeOption"
@change="onSearchFinish()"
/>
</a-form-item>
<a-form-item label="">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
style="width: 180px"
allow-clear
show-search
:filter-option="filterOption"
:options="statusOption"
@change="onSearchFinish()"
/>
</a-form-item>
</a-form>
<BasicTable
v-if="statusOption.length > 0"
ref="basicTable"
:columns="columns"
:list-url="getAllMapLayerTree"
:list-url="postPushHistoryList"
:search-params="searchParams"
>
<template #number="{ record }">
{{ record.pushedCount ?? 0 }}/{{ record.successCount ?? 0 }}/{{
record.pendingCount ?? 0
}}/{{ record.failCount ?? 0 }}
</template>
<template #status="{ record }">
{{ getname(record.status, statusOption) ?? '-' }}
</template>
<template #action="{ record }">
<div class="flex gap-[6px]">
<a-button
class="!p-0"
type="link"
size="small"
@click="handleEdit(record)"
>编辑</a-button
@click="showPushContent(record)"
>推送内容</a-button
>
<a-button
v-if="record.children && record.children.length == 0"
class="!p-0"
type="link"
size="small"
@click="handleEditInterface(record)"
>编辑接口</a-button
>
<a-button
class="!p-0"
type="link"
size="small"
@click="handleAdd(record)"
>新增子图层</a-button
>
<a-button
class="!p-0"
type="link"
size="small"
danger
@click="handleDelete(record)"
>删除</a-button
<a-button class="!p-0" type="link" size="small" @click="showPushStatus(record)"
>推送情况</a-button
>
</div>
</template>
<template #enable="{ record }">
<a-switch
:checked="record.enable === 1"
@change="(checked: boolean) => handleEnableChange(record, checked)"
/>
</template>
<template #checkable="{ record }">
<a-switch
:checked="record.checkable === 1"
@change="(checked: boolean) => handleCheckableChange(record, checked)"
/>
</template>
</BasicTable>
<LayerManagementForm
ref="layerManagementForm"
v-model:visible="editModalVisible"
:initial-values="currentRecord"
:is-interface="isInterface"
:loading="editLoading"
@cancel="editModalCancel"
@ok="handleEditSubmit"
/> -->
<a-modal
v-model:open="pushContentVisible"
title="推送内容"
:footer="null"
width="600px"
>
<div class="push-content-detail">
<div class="detail-item">
<span class="label">类型</span>
<span class="value">{{ pushContentData.type ?? '-' }}</span>
</div>
<div class="detail-item">
<span class="label">标题</span>
<span class="value">{{ pushContentData.title ?? '-' }}</span>
</div>
<div class="detail-item">
<span class="label">内容</span>
<span class="value content">{{
pushContentData.content ?? '-'
}}</span>
</div>
<div class="detail-item">
<span class="label">推送时间</span>
<span class="value">{{ pushContentData.planTime ?? '-' }}</span>
</div>
<div class="detail-item">
<span class="label">来源</span>
<span class="value">{{
getname(pushContentData.source, statusOption) ?? '-'
}}</span>
</div>
<div class="detail-item">
<span class="label">关联</span>
<span class="value">{{ pushContentData.relation ?? '-' }}</span>
</div>
</div>
</a-modal>
<PushStatusModal ref="pushStatusModalRef" v-model:visible="pushStatusVisible" :record="recordArr" />
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue';
import { ref, reactive, onMounted, nextTick } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import LayerManagementSearch from './LayerManagementSearch.vue';
import LayerManagementForm from './LayerManagementForm.vue';
import { message, Modal } from 'ant-design-vue';
import {
getAllMapLayerTree,
saveMapLayer,
deleteMapLayer
} from '@/api/system/map/LayerManagement';
//
import { postPushHistoryList } from '@/api/system/map/pushManagement';
import { getDictItemsByCode } from '@/api/dict';
import PushStatusModal from './PushStatusModal/index.vue';
import { useDraggable } from '@/utils/drag';
const columns = [
{
title: '',
dataIndex: '',
key: '',
width: 50,
customRender: () => {
return '';
}
title: '生成时间',
dataIndex: 'planTime',
key: 'planTime'
},
{
title: '序号',
key: 'index',
width: 60,
align: 'center',
customRender: ({ record, index }: any) => {
// children
// parentId
if (!record.parentId) {
// dataSource
let rootIndex = 0;
for (let i = 0; i < basicTable.value.tableData.length; i++) {
const item = basicTable.value.tableData[i];
// parentId
if (!item.parentId) {
rootIndex++;
//
if (
item.id === record.id ||
(item.key === record.key && item.id === record.id)
) {
return rootIndex;
}
}
}
return rootIndex;
} else {
// children
const findParentAndIndex = (data: any[], parentId: string): number => {
for (const item of data) {
if (item.key === parentId || item.id === parentId) {
// children
if (item.children && item.children.length > 0) {
const childIndex = item.children.findIndex(
(child: any) =>
child.key === record.key || child.id === record.id
);
return childIndex + 1;
}
}
//
if (item.children && item.children.length > 0) {
const result = findParentAndIndex(item.children, parentId);
if (result > 0) {
return result;
}
}
}
return 0;
};
return findParentAndIndex(basicTable.value.tableData, record.parentId);
}
}
title: '消息类型',
dataIndex: 'messageType',
key: 'messageType'
},
{
title: '图层名称',
title: '标题',
dataIndex: 'title',
key: 'title',
width: 150
key: 'title'
},
{
title: '已推送/成功/待推送/失败',
dataIndex: 'number',
key: 'number',
slots: { customRender: 'number' }
},
{
title: '图层编码',
dataIndex: 'code',
key: 'code',
width: 120
},
{
title: '标签显示级别',
dataIndex: 'treeLevel',
key: 'treeLevel',
width: 100
},
{
title: '是否启用',
dataIndex: 'enable',
key: 'enable',
width: 80,
slots: { customRender: 'enable' }
},
{
title: '是否可选',
dataIndex: 'checkable',
key: 'checkable',
width: 80,
slots: { customRender: 'checkable' }
},
{
title: '备注',
dataIndex: 'description',
key: 'description'
},
{
title: '创建人',
key: 'displayRecordUser',
dataIndex: 'displayRecordUser'
},
{
title: '创建时间',
key: 'recordTime',
dataIndex: 'recordTime',
width: 150
},
{
title: '更新时间',
key: 'modifyTime',
dataIndex: 'modifyTime',
width: 150
title: '状态',
key: 'status',
dataIndex: 'status',
slots: { customRender: 'status' }
},
{
title: '操作',
key: 'action',
@ -211,139 +164,133 @@ const columns = [
slots: { customRender: 'action' }
}
];
//
//
const basicTable = ref<any>(null);
//
const searchParams = ref({});
//
const isInterface = ref(false);
const editModalVisible = ref(false);
const editLoading = ref(false);
const parentId = ref<string | null>(null);
const currentRecord = ref<any | null>(null);
//
const handleAdd = (record: any) => {
currentRecord.value = null;
parentId.value = record.id;
editModalVisible.value = true;
};
// '-'
const filterRecord = (record: any) => {
const filtered: any = {};
const keepKeys = ['code']; //
Object.keys(record).forEach(key => {
if (keepKeys.includes(key) || record[key] !== '-') {
filtered[key] = record[key];
const pushStatusModalRef = ref<any>(null);
const searchParams = ref({
sort: [
{
field: 'createdAt',
dir: 'desc'
}
]
});
return filtered;
};
//
const handleEdit = (record: any) => {
console.log('handleEdit', record);
parentId.value = null;
isInterface.value = false;
currentRecord.value = filterRecord(record);
editModalVisible.value = true;
};
//
const handleEditInterface = (record: any) => {
console.log('handleEdit', record);
currentRecord.value = filterRecord(record);
console.log('handleEdit', currentRecord.value);
isInterface.value = true;
parentId.value = null;
editModalVisible.value = true;
};
//
const handleDelete = async (record: any) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除选中的记录吗?',
zIndex: 2002,
onOk: async () => {
try {
let res = await deleteMapLayer([record.id]);
message.success('删除成功');
basicTable.value.refresh();
} catch (error) {
message.error('删除失败');
return;
}
}
//
const searchForm = reactive({
status: undefined, //
messageTypeCode: undefined, //
title: undefined, //
planTime: undefined //
});
};
//
const onSearchFinish = (values: any) => {
searchParams.value = values;
nextTick(() => {
basicTable.value.getList();
const messageTypeCodeOption = ref([]);
const statusOption = ref([]);
const pushContentVisible = ref(false);
const pushContentData = reactive<any>({
type: '',
title: '',
content: '',
planTime: '',
source: '',
relation: ''
});
const pushStatusVisible = ref(false);
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
//
const handleReset = () => {
searchParams.value = {};
nextTick(() => {
basicTable.value.refresh();
const onSearchFinish = () => {
const filters = [];
if (searchForm.status !== undefined && searchForm.status !== null) {
filters.push({
field: 'status',
operator: 'eq',
dataType: 'string',
value: searchForm.status
});
};
//
const handleEnableChange = (record: any, checked: boolean) => {
currentRecord.value = filterRecord(record);
console.log(currentRecord.value);
record.enable = checked ? 1 : 0;
handleEditSubmit({ enable: checked ? 1 : 0 }, 'switch');
};
//
const handleCheckableChange = (record: any, checked: boolean) => {
currentRecord.value = filterRecord(record);
record.checkable = checked ? 1 : 0;
handleEditSubmit({ checkable: checked ? 1 : 0 }, 'switch');
};
//
const editModalCancel = () => {
isInterface.value = false;
parentId.value = null;
currentRecord.value = null;
editModalVisible.value = false;
};
const handleEditSubmit = async (values: any, type: string) => {
editLoading.value = true;
try {
let res = await saveMapLayer({
...currentRecord.value,
...values,
parentId: parentId.value
}
if (
searchForm.messageTypeCode !== undefined &&
searchForm.messageTypeCode !== null
) {
filters.push({
field: 'messageTypeCode',
operator: 'eq',
dataType: 'string',
value: searchForm.messageTypeCode
});
if (res.code == 0) {
if (type === 'switch') {
message.success('修改状态成功');
} else {
message.success(`保存成功`);
}
editModalVisible.value = false;
basicTable.value.refresh();
} else {
if (type === 'switch') {
message.success('修改状态成功');
} else {
message.success(`保存成功`);
if (
searchForm.title !== undefined &&
searchForm.title !== null &&
searchForm.title !== ''
) {
filters.push({
field: 'title',
operator: 'contains',
dataType: 'string',
value: searchForm.title
});
}
if (searchForm.planTime && searchForm.planTime.length === 2) {
filters.push({
field: 'planTime',
operator: 'gte',
dataType: 'string',
value: searchForm.planTime[0] + ' 00:00:00'
});
filters.push({
field: 'planTime',
operator: 'lte',
dataType: 'string',
value: searchForm.planTime[1] + ' 23:59:59'
});
}
} catch (error) {
if (type === 'switch') {
message.error('修改状态失败');
} else {
message.error(`保存失败`);
}
} finally {
editLoading.value = false;
const params = {
logic: 'and',
filters
};
basicTable.value.getList(params);
};
const getDict = async () => {
let res = await getDictItemsByCode({ dictCode: 'category' }); //
let res1 = await getDictItemsByCode({ dictCode: 'push_status' }); //
messageTypeCodeOption.value = res.data.map((item: any) => ({
value: item.itemCode,
label: item.dictName
}));
statusOption.value = res1.data.map((item: any) => ({
value: item.itemCode,
label: item.dictName
}));
};
//
const getname = (value: string, option: any) => {
return option.find((item: any) => item.value === value)?.label;
};
const showPushContent = (record: any) => {
pushContentData.type = record.type || '';
pushContentData.title = record.title || '';
pushContentData.content = record.content || '';
pushContentData.planTime = record.planTime || '';
pushContentData.source = record.source || '';
pushContentData.relation = record.relation || '';
pushContentVisible.value = true;
};
useDraggable(pushContentVisible, { boundary: true, resetOnOpen: true });
const recordArr:any = ref([])
const showPushStatus = async (record: any) => {
recordArr.value = record;
pushStatusVisible.value = true;
await nextTick();
await nextTick();
if (pushStatusModalRef.value) {
pushStatusModalRef.value.handleSearch();
}
};
onMounted(async () => {
await getDict();
onSearchFinish();
});
</script>
<style scoped lang="scss">
@ -355,4 +302,23 @@ const handleEditSubmit = async (values: any, type: string) => {
width: 17px !important;
margin: 0 !important;
}
.push-content-detail {
.detail-item {
display: flex;
margin-bottom: 12px;
line-height: 1.6;
.label {
font-weight: 500;
min-width: 80px;
flex-shrink: 0;
}
.value {
flex: 1;
word-break: break-all;
&.content {
white-space: pre-wrap;
}
}
}
}
</style>