20260802:大屏技术

This commit is contained in:
zhugongping 2026-08-02 07:52:40 +08:00
parent 0f96d0236d
commit 0a3051d4a7
31 changed files with 2491 additions and 639 deletions

View File

@ -35,8 +35,35 @@
"Bash(grep:*)", "Bash(grep:*)",
"mcp__filesystem__list_directory_with_sizes", "mcp__filesystem__list_directory_with_sizes",
"Bash(find /Users/admin/IdeaProjects/fastbee -type d -name *oss* -o -type d -name *storage* -o -type d -name *plugs*)", "Bash(find /Users/admin/IdeaProjects/fastbee -type d -name *oss* -o -type d -name *storage* -o -type d -name *plugs*)",
"Bash(source ~/.nvm/nvm.sh)" "Bash(source ~/.nvm/nvm.sh)",
"Bash(curl:*)",
"Bash(xargs cat:*)",
"Bash(head -20 echo ---status--- git status --porcelain -- *Project* yack-web/src/views/bigScreen/*)",
"Bash(find /Users/admin/IdeaProjects/fastbee -type f -name *home.vue)",
"Bash(find /Users/admin/IdeaProjects/fastbee -type f -path **/domain/Group.java)",
"Bash(find /Users/admin/IdeaProjects/fastbee/yack-web/src/views -type d -name *project* -o -type f -name *project*.vue)",
"Bash(find /Users/admin/IdeaProjects/fastbee -name *init*.sql -o -name *yack*.sql)",
"Bash(find . -name *.log -type f -mmin -30)",
"Bash(xmllint --noout /Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ProjectMapper.xml)",
"Bash(xmllint --noout fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml)",
"Bash(xmllint --noout fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ProjectMapper.xml)",
"Bash(./node_modules/.bin/eslint src/views/iot/project/device-select.vue src/views/iot/project/index.vue src/views/iot/device/index.vue src/views/bigScreen/home.vue)",
"Bash(./node_modules/.bin/eslint --no-ignore src/views/iot/project/device-select.vue src/views/iot/project/index.vue src/views/iot/device/index.vue src/views/bigScreen/home.vue)",
"Bash(xmllint --noout /Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml)",
"mcp__filesystem__write_file",
"Bash(convert /Users/admin/IdeaProjects/fastbee/yack-web/src/assets/logo/yack.png -format \"%[fx:mean.r],%[fx:mean.g],%[fx:mean.b]\\\\n\" info:)",
"Bash(find /Users/admin/IdeaProjects/fastbee -name SysProfileController.java -not -path */target/*)",
"Bash(node -e \":*)",
"Bash(./node_modules/.bin/eslint src/views/bigScreen/home.vue)",
"Bash(./node_modules/.bin/eslint --no-ignore src/views/bigScreen/home.vue)",
"Bash(./node_modules/.bin/eslint --no-eslintrc --env es6 src/views/bigScreen/home.vue)",
"Read(//private/tmp/**)",
"Bash(for TK in b920e6076a06aa279c974f5f76cf746e c9ef88370bacf8f51662c01ad80a580c)",
"Bash(do echo:*)",
"Bash(done)",
"Bash(file /tmp/tile_*.png)",
"Bash(for REF in \"http://localhost:8080/\" \"https://dev.fastbee.cn/\" \"https://www.tianditu.gov.cn/\")",
"Read(//tmp/**)"
] ]
} }
} }

View File

@ -8,8 +8,10 @@ TARGET_PORT = 8888 # 请根据实际情况修改
# 元数组 modbus 指令 # 元数组 modbus 指令
MODBUS_DATA_HEX = ( MODBUS_DATA_HEX = (
"7e80DFEB502006887e", "7e80D4F8E47AFB347e",
"01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99" "01031c42310b2f41a3000040ae48850000000042b166ea3fa487e43e3fefd65e78",
"02031c422a945a41b4000040a3f8210000000042a7fd174081531ec08dec058dc0",
"03031c4223b88041970000409c54a30000000042aa4f04bfa6c19fc095539b4f18"
) )
# 重要-四川远德MQTT推送 (倾角计) # 重要-四川远德MQTT推送 (倾角计)
@ -21,6 +23,10 @@ MODBUS_DATA_HEX = (
# "020320413b9fa441acd34dc0024a003fa2a59ac2af34a0c18e17ea413167dbc3f9c7fe6d40", # "020320413b9fa441acd34dc0024a003fa2a59ac2af34a0c18e17ea413167dbc3f9c7fe6d40",
# "0303204139565641ab21ddbff6af333fe26800c2aec626c18685244176ee82c3f9bd7568c8", # "0303204139565641ab21ddbff6af333fe26800c2aec626c18685244176ee82c3f9bd7568c8",
# "040320413a0d3e41a534d3bf461ccd403d7e00c2ade1e4c0d819c441ce9ca7c3f9a4d260bf" # "040320413a0d3e41a534d3bf461ccd403d7e00c2ade1e4c0d819c441ce9ca7c3f9a4d260bf"
# 静力水准仪
# "01031c41aa0c7941d78000402000220000000042b121523fb1fb373eb5668c9cd8",
# "02031c419b24c741e18000400bf3910000000042a83e6840824127c0877958c7d0",
# "03031c418eb64a41cb00003ff8d8130000000042aaaa05bf95093cc090a1de8803"
# 01 03 1c 4268 442f 41d3 0000 418b 6685 0000 0000 4026 f4ca c00d 1fd3 c2ad 2a9d 6311 # 01 03 1c 4268 442f 41d3 0000 418b 6685 0000 0000 4026 f4ca c00d 1fd3 c2ad 2a9d 6311
# 从机地址 功能码 字节长度 2个字节 CRC校验 # 从机地址 功能码 字节长度 2个字节 CRC校验

View File

@ -130,6 +130,16 @@ public class DeviceController extends BaseController {
return getDataTable(deviceService.selectDeviceShortList(device)); return getDataTable(deviceService.selectDeviceShortList(device));
} }
/**
* 查询当前用户可见设备统计,大屏首页使用(与 shortList 相同范围)
*/
@PreAuthorize("@ss.hasAnyPermi('iot:device:list,iot:scene:list,dataCenter:history:list,dataCenter:analysis:list,dataCenter:repair:list')")
@GetMapping("/userShortStatistic")
@ApiOperation("查询当前用户可见设备统计")
public AjaxResult userShortStatistic(Device device) {
return AjaxResult.success(deviceService.selectUserDeviceStatistic(device));
}
/** /**
* 查询所有设备简短列表 * 查询所有设备简短列表
*/ */

View File

@ -65,22 +65,9 @@ public class ProjectController extends BaseController {
@ApiOperation("获取项目详情") @ApiOperation("获取项目详情")
public AjaxResult getInfo(@PathVariable("projectId") Long projectId) { public AjaxResult getInfo(@PathVariable("projectId") Long projectId) {
Project project = projectService.selectProjectById(projectId); Project project = projectService.selectProjectById(projectId);
if (project != null) {
project.setGroupIds(projectService.selectGroupIdsByProjectId(projectId));
}
return AjaxResult.success(project); return AjaxResult.success(project);
} }
/**
* 获取项目下的所有关联分组ID数组
*/
@PreAuthorize("@ss.hasPermi('iot:project:query')")
@GetMapping(value = "/getGroupIds/{projectId}")
@ApiOperation("获取项目下的所有关联分组ID数组")
public AjaxResult getGroupIds(@PathVariable("projectId") Long projectId) {
return AjaxResult.success(projectService.selectGroupIdsByProjectId(projectId));
}
/** /**
* 新增项目 * 新增项目
*/ */
@ -104,18 +91,29 @@ public class ProjectController extends BaseController {
} }
/** /**
* 更新项目下的关联分组 * 查询项目关联的设备ID数组
*/
@PreAuthorize("@ss.hasAnyPermi('iot:project:query,iot:project:list')")
@GetMapping("/deviceIds/{projectId}")
@ApiOperation("查询项目关联的设备ID数组")
public AjaxResult getDeviceIds(@PathVariable Long projectId) {
Long[] deviceIds = projectService.selectDeviceIdsByProjectId(projectId);
return AjaxResult.success(deviceIds);
}
/**
* 更新项目下的关联设备
* *
* @param projectId 项目ID * @param projectId 项目ID
* @param groupIds 分组ID数组 * @param deviceIds 设备ID数组
* @return 结果 * @return 结果
*/ */
@PreAuthorize("@ss.hasPermi('iot:project:edit')") @PreAuthorize("@ss.hasPermi('iot:project:edit')")
@Log(title = "项目分组", businessType = BusinessType.UPDATE) @Log(title = "项目设备", businessType = BusinessType.UPDATE)
@PutMapping("/updateProjectGroups") @PutMapping("/updateProjectDevices")
@ApiOperation("更新项目下的关联分组") @ApiOperation("更新项目下的关联设备")
public AjaxResult updateProjectGroups(@RequestParam Long projectId, @RequestBody Long[] groupIds) { public AjaxResult updateProjectDevices(@RequestParam Long projectId, @RequestBody Long[] deviceIds) {
return toAjax(projectService.updateProjectGroups(projectId, groupIds)); return toAjax(projectService.updateProjectDevices(projectId, deviceIds));
} }
/** /**

View File

@ -202,13 +202,10 @@ public class CoordinateTransformUtils {
double[] bd = CoordinateTransformUtils.gcj02ToBd09(gcj[0], gcj[1]); double[] bd = CoordinateTransformUtils.gcj02ToBd09(gcj[0], gcj[1]);
CoordinateTransformUtils.printConvert("GCJ-02", gcj[0], gcj[1], "BD-09", bd); CoordinateTransformUtils.printConvert("GCJ-02", gcj[0], gcj[1], "BD-09", bd);
// BD-09 → GCJ-02 // GCJ-02 → WGS-84 112.87,28.12
double[] gcjBack = CoordinateTransformUtils.bd09ToGcj02(112.945473,28.234889); double[] GCJ02 = {112.87,28.12};
CoordinateTransformUtils.printConvert("BD-09", bd[0], bd[1], "GCJ-02", gcjBack); double[] wgsBack = CoordinateTransformUtils.gcj02ToWgs84(GCJ02[0], GCJ02[1]);
CoordinateTransformUtils.printConvert("GCJ-02", GCJ02[0], GCJ02[1], "WGS-84", wgsBack);
// GCJ-02 → WGS-84 111.944921,28.809167
double[] wgsBack = CoordinateTransformUtils.gcj02ToWgs84(111.944921,28.809167);
CoordinateTransformUtils.printConvert("GCJ-02", gcjBack[0], gcjBack[1], "WGS-84", wgsBack);
// 直接计算 WGS-84 → BD-09 // 直接计算 WGS-84 → BD-09
double[] directBd = CoordinateTransformUtils.wgs84ToBd09(wgsLng, wgsLat); double[] directBd = CoordinateTransformUtils.wgs84ToBd09(wgsLng, wgsLat);

View File

@ -178,15 +178,13 @@ public class Session {
/*设备端注册*/ /*设备端注册*/
public void register(Message message) { public void register(Message message) {
register(message.getClientId(), message); String clientId = message.getClientId();
}
public void register(String clientId, Message message) {
//已经注册,不再发送注册包数据 //已经注册,不再发送注册包数据
if (isRegistered()) { if (isRegistered()) {
return; return;
} }
if (clientId == null) { if (clientId == null) {
log.error("messageId:{},serNo:{},channelId:{}", message.getMessageId(), message.getSerNo(), message.getChannelId());
throw new ServiceException("客户端注册clientId不能为空"); throw new ServiceException("客户端注册clientId不能为空");
} }
this.clientId = clientId.toUpperCase(); this.clientId = clientId.toUpperCase();

View File

@ -227,6 +227,19 @@ public class Device extends BaseEntity {
@TableField(exist = false) @TableField(exist = false)
private String transport; private String transport;
/**
* 项目设备选择查询模式(非数据库字段)
*/
@ApiModelProperty("项目设备选择查询模式")
@TableField(exist = false)
private Boolean projectDeviceMode;
/**
* 所属项目ID
*/
@ApiModelProperty("所属项目ID")
private Long projectId;
/** /**
* IMEI(蜂窝模块设备标识) * IMEI(蜂窝模块设备标识)
*/ */
@ -250,12 +263,12 @@ public class Device extends BaseEntity {
private Boolean showChild; private Boolean showChild;
@Transient @Transient
@ApiModelProperty("查询入口(1=设备管理列表 2=数据中心 3=全部设备)") @ApiModelProperty("查询入口(1=设备管理列表(需要查看更新时间) 2=数据联动、项目管理、设备绑定 3=全部设备)")
@TableField(exist = false) @TableField(exist = false)
private Integer entrance; private Integer entrance;
@TableField(exist = false) // @TableField(exist = false)
private List<Device> subDeviceList; // private List<Device> subDeviceList;
/** /**
* 删除标志(0代表存在 2代表删除) * 删除标志(0代表存在 2代表删除)
@ -289,9 +302,4 @@ public class Device extends BaseEntity {
@TableField(exist = false) @TableField(exist = false)
private Boolean canConfigPoll = false; private Boolean canConfigPoll = false;
public static long getSerialVersionUID() {
return serialVersionUID;
}
} }

View File

@ -121,12 +121,6 @@ public class Project extends BaseEntity {
@ApiModelProperty("删除标志") @ApiModelProperty("删除标志")
private String delFlag; private String delFlag;
/**
* 关联分组ID数组(用于项目-分组关系管理)
*/
@ApiModelProperty("关联分组ID数组")
private Long[] groupIds;
/** /**
* 项目关联设备总数 * 项目关联设备总数
*/ */
@ -265,14 +259,6 @@ public class Project extends BaseEntity {
this.delFlag = delFlag; this.delFlag = delFlag;
} }
public Long[] getGroupIds() {
return groupIds;
}
public void setGroupIds(Long[] groupIds) {
this.groupIds = groupIds;
}
public Integer getDeviceCount() { public Integer getDeviceCount() {
return deviceCount; return deviceCount;
} }
@ -320,7 +306,6 @@ public class Project extends BaseEntity {
.append("updateBy", getUpdateBy()) .append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime()) .append("updateTime", getUpdateTime())
.append("remark", getRemark()) .append("remark", getRemark())
.append("groupIds", getGroupIds())
.append("deviceCount", getDeviceCount()) .append("deviceCount", getDeviceCount())
.append("onlineCount", getOnlineCount()) .append("onlineCount", getOnlineCount())
.append("offlineCount", getOfflineCount()) .append("offlineCount", getOfflineCount())

View File

@ -145,6 +145,22 @@ public interface DeviceMapper extends BaseMapperX<Device> {
*/ */
public List<DeviceShortOutput> selectDeviceShortList(Device device); public List<DeviceShortOutput> selectDeviceShortList(Device device);
/**
* 查询当前用户可见设备统计(与 selectDeviceShortList 相同范围)
*
* @param device 设备(携带 deptId/tenantId/showChild/entrance 等筛选条件)
* @return 设备统计
*/
public DeviceStatistic selectDeviceShortStatistic(Device device);
/**
* 查询终端用户可见设备统计(与 listTerminalUser 相同范围)
*
* @param device 设备(携带 tenantId)
* @return 设备统计
*/
public DeviceStatistic selectTerminalUserDeviceStatistic(Device device);
/** /**
* 查询所有设备简短列表 * 查询所有设备简短列表
* *
@ -416,4 +432,30 @@ public interface DeviceMapper extends BaseMapperX<Device> {
*/ */
public List<String> selectSerialNumbersByProductId(@Param("productId") Long productId); public List<String> selectSerialNumbersByProductId(@Param("productId") Long productId);
/**
* 批量更新设备的项目归属
*
* @param projectId 项目ID
* @param deviceIds 设备ID数组
* @return 结果
*/
int updateDeviceProjectByIds(@Param("projectId") Long projectId, @Param("deviceIds") Long[] deviceIds);
/**
* 清除项目下除指定设备外的其他设备归属
*
* @param projectId 项目ID
* @param deviceIds 保留的设备ID数组(这些设备保持归属该项目)
* @return 结果
*/
int clearDeviceProjectExcept(@Param("projectId") Long projectId, @Param("deviceIds") Long[] deviceIds);
/**
* 清除一个或多个项目下的设备归属
*
* @param projectIds 项目ID数组
* @return 结果
*/
int clearDeviceProjectByProjectIds(@Param("projectIds") Long[] projectIds);
} }

View File

@ -33,12 +33,12 @@ public interface ProjectMapper {
public List<Project> selectProjectList(Project project); public List<Project> selectProjectList(Project project);
/** /**
* 通过项目ID查询关联的分组ID数组 * 通过项目ID查询关联的设备ID数组
* *
* @param projectId 项目ID * @param projectId 项目ID
* @return 分组ID数组 * @return 设备ID数组
*/ */
public Long[] selectGroupIdsByProjectId(Long projectId); public Long[] selectDeviceIdsByProjectId(Long projectId);
/** /**
* 根据负责人ID查询项目列表 * 根据负责人ID查询项目列表
@ -104,23 +104,6 @@ public interface ProjectMapper {
*/ */
public int updateProject(Project project); public int updateProject(Project project);
/**
* 批量插入项目-分组关系
*
* @param projectId 项目ID
* @param groupIds 分组ID数组
* @return 结果
*/
public int insertProjectGroups(@Param("projectId") Long projectId, @Param("groupIds") Long[] groupIds);
/**
* 删除项目关联的所有分组关系
*
* @param projectId 项目ID
* @return 结果
*/
public int deleteProjectGroupsByProjectId(Long projectId);
/** /**
* 批量删除项目 * 批量删除项目
* *

View File

@ -34,6 +34,11 @@ public class DeviceAllShortOutput {
*/ */
private Integer deviceType; private Integer deviceType;
/**
* 项目ID
*/
private Long projectId;
/** /**
* 用户昵称 * 用户昵称
*/ */
@ -203,6 +208,14 @@ public class DeviceAllShortOutput {
this.deviceType = deviceType; this.deviceType = deviceType;
} }
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public String getUserName() { public String getUserName() {
return userName; return userName;
} }

View File

@ -57,6 +57,20 @@ public class DeviceShortOutput {
@Excel(name = "设备编号") @Excel(name = "设备编号")
private String serialNumber; private String serialNumber;
/**
* 项目ID
*/
private Long projectId;
/**
* 经度
*/
private BigDecimal longitude;
/**
* 纬度
*/
private BigDecimal latitude;
/** /**
* IMEI(蜂窝模块设备标识) * IMEI(蜂窝模块设备标识)
*/ */
@ -419,6 +433,30 @@ public class DeviceShortOutput {
this.serialNumber = serialNumber; this.serialNumber = serialNumber;
} }
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public BigDecimal getLongitude() {
return longitude;
}
public void setLongitude(BigDecimal longitude) {
this.longitude = longitude;
}
public BigDecimal getLatitude() {
return latitude;
}
public void setLatitude(BigDecimal latitude) {
this.latitude = latitude;
}
public String getImei() { public String getImei() {
return imei; return imei;
} }

View File

@ -147,6 +147,14 @@ public interface IDeviceService {
*/ */
public List<DeviceShortOutput> selectDeviceShortList(Device device); public List<DeviceShortOutput> selectDeviceShortList(Device device);
/**
* 查询当前用户可见设备统计(与 shortList 相同范围)
*
* @param device 设备筛选条件
* @return 设备统计
*/
public DeviceStatistic selectUserDeviceStatistic(Device device);
/** /**
* 新增设备 * 新增设备
* *

View File

@ -30,12 +30,12 @@ public interface IProjectService {
public List<Project> selectProjectList(Project project); public List<Project> selectProjectList(Project project);
/** /**
* 通过项目ID查询关联的分组ID数组 * 通过项目ID查询关联的设备ID数组
* *
* @param projectId 项目ID * @param projectId 项目ID
* @return 分组ID数组 * @return 设备ID数组
*/ */
public Long[] selectGroupIdsByProjectId(Long projectId); public Long[] selectDeviceIdsByProjectId(Long projectId);
/** /**
* 根据负责人ID查询项目列表 * 根据负责人ID查询项目列表
@ -102,13 +102,13 @@ public interface IProjectService {
public int updateProject(Project project); public int updateProject(Project project);
/** /**
* 更新项目关联的分组 * 更新项目关联的设备
* *
* @param projectId 项目ID * @param projectId 项目ID
* @param groupIds 分组ID数组 * @param deviceIds 设备ID数组
* @return 结果 * @return 结果
*/ */
public int updateProjectGroups(Long projectId, Long[] groupIds); public int updateProjectDevices(Long projectId, Long[] deviceIds);
/** /**
* 批量删除项目 * 批量删除项目

View File

@ -996,6 +996,7 @@ public class DeviceServiceImpl implements IDeviceService {
*/ */
@Override @Override
public List<DeviceShortOutput> selectDeviceShortList(Device device) { public List<DeviceShortOutput> selectDeviceShortList(Device device) {
long startTime = System.currentTimeMillis();
List<DeviceShortOutput> list = deviceMapper.selectDeviceShortList(device); List<DeviceShortOutput> list = deviceMapper.selectDeviceShortList(device);
if (CollectionUtils.isEmpty(list)) { if (CollectionUtils.isEmpty(list)) {
return new ArrayList<>(); return new ArrayList<>();
@ -1018,15 +1019,41 @@ public class DeviceServiceImpl implements IDeviceService {
} }
} }
fillShortListLatestReportTime(list, device.getEntrance()); fillShortListLatestReportTime(list, device.getEntrance());
log.warn("当前selectDeviceShortList查询接口耗时: {}", System.currentTimeMillis() - startTime);
return list; return list;
} }
/**
* 查询当前用户可见设备统计(与 shortList 相同范围)
*/
@Override
public DeviceStatistic selectUserDeviceStatistic(Device device) {
LoginUser loginUser = getLoginUser();
if (null == loginUser.getDeptId()) {
// 终端用户:设备归属(iot_device_user) + 分享(iot_device_share)
device.setTenantId(loginUser.getUserId());
DeviceStatistic statistic = deviceMapper.selectTerminalUserDeviceStatistic(device);
return statistic == null ? new DeviceStatistic() : statistic;
}
if (null == device.getDeptId()) {
device.setDeptId(loginUser.getDeptId());
}
if (Objects.isNull(device.getTenantId())) {
device.setTenantId(loginUser.getUserId());
}
if (null == device.getShowChild()) {
device.setShowChild(true);
}
DeviceStatistic statistic = deviceMapper.selectDeviceShortStatistic(device);
return statistic == null ? new DeviceStatistic() : statistic;
}
/** /**
* 短时列表:从 Redis 物模型缓存聚合各点位 ts最大值写入 updateTime(方案 B)。 * 短时列表:从 Redis 物模型缓存聚合各点位 ts最大值写入 updateTime(方案 B)。
* 网关设备自身往往无属性上报,数据在子设备 SN 的缓存中,需合并子设备的 ts。 * 网关设备自身往往无属性上报,数据在子设备 SN 的缓存中,需合并子设备的 ts。
*/ */
private void fillShortListLatestReportTime(List<DeviceShortOutput> list, Integer entrance) { private void fillShortListLatestReportTime(List<DeviceShortOutput> list, Integer entrance) {
if (CollectionUtils.isEmpty(list) && !Integer.valueOf(1).equals(entrance)) { if (CollectionUtils.isEmpty(list) || !Integer.valueOf(1).equals(entrance)) {
return; return;
} }
for (DeviceShortOutput row : list) { for (DeviceShortOutput row : list) {
@ -2186,8 +2213,10 @@ public class DeviceServiceImpl implements IDeviceService {
@Override @Override
public List<DeviceShortOutput> listTerminalUser(Device device) { public List<DeviceShortOutput> listTerminalUser(Device device) {
long startTime = System.currentTimeMillis();
List<DeviceShortOutput> list = deviceMapper.listTerminalUser(device); List<DeviceShortOutput> list = deviceMapper.listTerminalUser(device);
fillShortListLatestReportTime(list, device.getEntrance()); fillShortListLatestReportTime(list, device.getEntrance());
log.warn("当前listTerminalUser查询接口耗时: {}", System.currentTimeMillis() - startTime);
return list; return list;
} }

View File

@ -4,6 +4,7 @@ import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.core.domain.entity.SysUser; import com.fastbee.common.core.domain.entity.SysUser;
import com.fastbee.iot.domain.Device; import com.fastbee.iot.domain.Device;
import com.fastbee.iot.domain.Project; import com.fastbee.iot.domain.Project;
import com.fastbee.iot.mapper.DeviceMapper;
import com.fastbee.iot.mapper.ProjectMapper; import com.fastbee.iot.mapper.ProjectMapper;
import com.fastbee.iot.service.IProjectService; import com.fastbee.iot.service.IProjectService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -28,6 +29,9 @@ public class ProjectServiceImpl implements IProjectService {
@Autowired @Autowired
private ProjectMapper projectMapper; private ProjectMapper projectMapper;
@Autowired
private DeviceMapper deviceMapper;
/** /**
* 查询项目 * 查询项目
* *
@ -58,14 +62,14 @@ public class ProjectServiceImpl implements IProjectService {
} }
/** /**
* 通过项目ID查询关联的分组ID数组 * 通过项目ID查询关联的设备ID数组
* *
* @param projectId 项目ID * @param projectId 项目ID
* @return 分组ID数组 * @return 设备ID数组
*/ */
@Override @Override
public Long[] selectGroupIdsByProjectId(Long projectId) { public Long[] selectDeviceIdsByProjectId(Long projectId) {
return projectMapper.selectGroupIdsByProjectId(projectId); return projectMapper.selectDeviceIdsByProjectId(projectId);
} }
/** /**
@ -171,18 +175,11 @@ public class ProjectServiceImpl implements IProjectService {
project.setCreateBy(getLoginUser().getUsername()); project.setCreateBy(getLoginUser().getUsername());
project.setDelFlag("0"); project.setDelFlag("0");
int result = projectMapper.insertProject(project); return projectMapper.insertProject(project);
// 插入项目-分组关系
if (project.getGroupIds() != null && project.getGroupIds().length > 0) {
projectMapper.insertProjectGroups(project.getProjectId(), project.getGroupIds());
}
return result;
} }
/** /**
* 修改项目(含分组关联更新) * 修改项目
* *
* @param project 项目 * @param project 项目
* @return 结果 * @return 结果
@ -192,38 +189,30 @@ public class ProjectServiceImpl implements IProjectService {
public int updateProject(Project project) { public int updateProject(Project project) {
project.setUpdateTime(DateUtils.getNowDate()); project.setUpdateTime(DateUtils.getNowDate());
project.setUpdateBy(getLoginUser().getUsername()); project.setUpdateBy(getLoginUser().getUsername());
int rows = projectMapper.updateProject(project); return projectMapper.updateProject(project);
// 更新项目-分组关联关系(先删后增)
if (project.getProjectId() != null) {
projectMapper.deleteProjectGroupsByProjectId(project.getProjectId());
if (project.getGroupIds() != null && project.getGroupIds().length > 0) {
projectMapper.insertProjectGroups(project.getProjectId(), project.getGroupIds());
}
}
return rows;
} }
/** /**
* 更新项目关联的分组(事务管理) * 更新项目关联的设备(事务管理)
* *
* @param projectId 项目ID * @param projectId 项目ID
* @param groupIds 分组ID数组 * @param deviceIds 设备ID数组
* @return 结果 * @return 结果
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int updateProjectGroups(Long projectId, Long[] groupIds) { public int updateProjectDevices(Long projectId, Long[] deviceIds) {
projectMapper.deleteProjectGroupsByProjectId(projectId); // 先清除项目下不在新列表中的设备归属
if (groupIds != null && groupIds.length > 0) { deviceMapper.clearDeviceProjectExcept(projectId, deviceIds);
return projectMapper.insertProjectGroups(projectId, groupIds); // 再将新列表中的设备归属到本项目(覆盖式转移)
if (deviceIds != null && deviceIds.length > 0) {
deviceMapper.updateDeviceProjectByIds(projectId, deviceIds);
} }
return 1; return 1;
} }
/** /**
* 批量删除项目(同时清理关联分组) * 批量删除项目
* *
* @param projectIds 需要删除的项目主键集合 * @param projectIds 需要删除的项目主键集合
* @return 结果 * @return 结果
@ -231,15 +220,12 @@ public class ProjectServiceImpl implements IProjectService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int deleteProjectByIds(Long[] projectIds) { public int deleteProjectByIds(Long[] projectIds) {
// 先清理关联分组关系 deviceMapper.clearDeviceProjectByProjectIds(projectIds);
for (Long projectId : projectIds) {
projectMapper.deleteProjectGroupsByProjectId(projectId);
}
return projectMapper.deleteProjectByIds(projectIds); return projectMapper.deleteProjectByIds(projectIds);
} }
/** /**
* 删除项目信息(同时清理关联分组) * 删除项目信息
* *
* @param projectId 项目主键 * @param projectId 项目主键
* @return 结果 * @return 结果
@ -247,7 +233,7 @@ public class ProjectServiceImpl implements IProjectService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int deleteProjectById(Long projectId) { public int deleteProjectById(Long projectId) {
projectMapper.deleteProjectGroupsByProjectId(projectId); deviceMapper.clearDeviceProjectByProjectIds(new Long[]{projectId});
return projectMapper.deleteProjectById(projectId); return projectMapper.deleteProjectById(projectId);
} }

View File

@ -38,6 +38,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="protocolCode" column="protocol_code" /> <result property="protocolCode" column="protocol_code" />
<result property="imei" column="imei" /> <result property="imei" column="imei" />
<result property="iccid" column="iccid" /> <result property="iccid" column="iccid" />
<result property="projectId" column="project_id" />
</resultMap> </resultMap>
<!-- <!--
@ -80,6 +81,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="guid" column="guid" /> <result property="guid" column="guid" />
<result property="imei" column="imei" /> <result property="imei" column="imei" />
<result property="iccid" column="iccid" /> <result property="iccid" column="iccid" />
<result property="projectId" column="project_id" />
<result property="longitude" column="longitude" />
<result property="latitude" column="latitude" />
</resultMap> </resultMap>
<resultMap type="com.fastbee.iot.model.DeviceAllShortOutput" id="DeviceAllShortResult"> <resultMap type="com.fastbee.iot.model.DeviceAllShortOutput" id="DeviceAllShortResult">
@ -102,6 +106,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="subDeviceCount" column="sub_device_count"/> <result property="subDeviceCount" column="sub_device_count"/>
<result property="imei" column="imei" /> <result property="imei" column="imei" />
<result property="iccid" column="iccid" /> <result property="iccid" column="iccid" />
<result property="projectId" column="project_id" />
</resultMap> </resultMap>
<resultMap type="com.fastbee.iot.model.UserAndTenant" id="UserAndTenantResult"> <resultMap type="com.fastbee.iot.model.UserAndTenant" id="UserAndTenantResult">
@ -261,6 +266,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="productName != null and productName != ''"> and product_name like concat('%', #{productName}, '%')</if> <if test="productName != null and productName != ''"> and product_name like concat('%', #{productName}, '%')</if>
<if test="tenantId != null "> and tenant_id = #{tenantId}</if> <if test="tenantId != null "> and tenant_id = #{tenantId}</if>
<if test="tenantName != null and tenantName != ''"> and tenant_name like concat('%', #{tenantName}, '%')</if> <if test="tenantName != null and tenantName != ''"> and tenant_name like concat('%', #{tenantName}, '%')</if>
<if test="projectId != null and projectId != 0"> and project_id = #{projectId}</if>
<if test="serialNumber != null and serialNumber != ''"> and serial_number = #{serialNumber}</if> <if test="serialNumber != null and serialNumber != ''"> and serial_number = #{serialNumber}</if>
<if test="status != null "> and status = #{status}</if> <if test="status != null "> and status = #{status}</if>
<if test="networkAddress != null and networkAddress != ''"> and network_address like concat('%', #{networkAddress}, '%')</if> <if test="networkAddress != null and networkAddress != ''"> and network_address like concat('%', #{networkAddress}, '%')</if>
@ -370,11 +376,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectAllDeviceShortList" parameterType="com.fastbee.iot.domain.Device" resultMap="DeviceAllShortResult"> <select id="selectAllDeviceShortList" parameterType="com.fastbee.iot.domain.Device" resultMap="DeviceAllShortResult">
select d.device_id, d.device_name, d.product_name,p.device_type, d.tenant_name, d.serial_number,d.gw_dev_code, d.firmware_version, d.status,d.rssi,d.is_shadow , select d.device_id, d.device_name, d.product_name,p.device_type, d.tenant_name, d.serial_number,d.gw_dev_code, d.firmware_version, d.status,d.rssi,d.is_shadow ,
d.location_way, d.active_time,d.network_address,d.longitude,d.latitude d.location_way, d.active_time,d.network_address,d.longitude,d.latitude,d.project_id
from iot_device d from iot_device d
left join iot_product p on p.product_id=d.product_id left join iot_product p on p.product_id=d.product_id
<where> <where>
<if test="productId != null "> and d.product_id = #{productId}</if> <if test="productId != null "> and d.product_id = #{productId}</if>
<if test="projectId != null and projectId != 0"> and d.project_id = #{projectId}</if>
<if test="deptId != null and deptId != 0 and showChild != null and showChild"> <if test="deptId != null and deptId != 0 and showChild != null and showChild">
and d.tenant_id in (SELECT u.user_id FROM sys_user u and d.tenant_id in (SELECT u.user_id FROM sys_user u
join sys_dept de on u.dept_id = de.dept_id join sys_dept de on u.dept_id = de.dept_id
@ -409,18 +416,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
d.tenant_id, d.tenant_name, d.serial_number,d.gw_dev_code, d.tenant_id, d.tenant_name, d.serial_number,d.gw_dev_code,
d.firmware_version, d.status,d.rssi,d.is_shadow,d.is_simulate ,d.location_way, d.firmware_version, d.status,d.rssi,d.is_shadow,d.is_simulate ,d.location_way,
d.things_model_value, d.active_time,d.create_time, if(null = d.img_url or '' = d.img_url, p.img_url, d.img_url) as img_url, d.things_model_value, d.active_time,d.create_time, if(null = d.img_url or '' = d.img_url, p.img_url, d.img_url) as img_url,
d.longitude, d.latitude,
case case
when (select count(device_id) from iot_device_share du where du.device_id = d.device_id and du.user_id = #{tenantId}) then 0 when (select count(device_id) from iot_device_share du where du.device_id = d.device_id and du.user_id = #{tenantId}) then 0
else 1 else 1
end as is_owner, end as is_owner,
(select count(1) from iot_device d1 where d1.gw_dev_code = d.serial_number) as sub_device_count, (select count(1) from iot_device d1 where d1.gw_dev_code = d.serial_number) as sub_device_count,
p.protocol_code,p.transport,p.guid p.protocol_code,p.transport,p.guid,d.project_id
from iot_device d from iot_device d
left join iot_product p on p.product_id=d.product_id left join iot_product p on p.product_id=d.product_id
<if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if> <if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if>
<where> <where>
and d.gw_dev_code is null and d.gw_dev_code is null
<if test="entrance != null and entrance == 1"> <if test="entrance != null and (entrance == 1 or entrance == 2)">
and (p.device_type is null or p.device_type != 4) and (p.device_type is null or p.device_type != 4)
</if> </if>
<if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if> <if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if>
@ -432,6 +440,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="gwDevCode != null and gwDevCode != ''"> and d.gw_dev_code = #{gwDevCode}</if> <if test="gwDevCode != null and gwDevCode != ''"> and d.gw_dev_code = #{gwDevCode}</if>
<if test="status != null "> and d.status = #{status}</if> <if test="status != null "> and d.status = #{status}</if>
<if test="isSimulate != null">and d.is_simulate = #{isSimulate}</if> <if test="isSimulate != null">and d.is_simulate = #{isSimulate}</if>
<if test="projectDeviceMode == null or !projectDeviceMode">
<if test="projectId != null and projectId != 0"> and d.project_id = #{projectId}</if>
</if>
<if test="deptId != null and deptId != 0 and showChild != null and showChild"> <if test="deptId != null and deptId != 0 and showChild != null and showChild">
and and
( d.tenant_id in (SELECT u.user_id FROM sys_user u ( d.tenant_id in (SELECT u.user_id FROM sys_user u
@ -446,12 +457,62 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
or d.device_id in (select du.device_id from iot_device_share du where du.user_id = #{tenantId}) or d.device_id in (select du.device_id from iot_device_share du where du.user_id = #{tenantId})
) )
</if> </if>
<if test="params.beginActiveTime != null and params.beginActiveTime != '' and params.endActiveTime != null and params.endActiveTime != ''"> and d.active_time between #{params.beginActiveTime} and #{params.endActiveTime}</if>
</where> </where>
group by d.device_id,d.tenant_id group by d.device_id,d.tenant_id
order by d.create_time desc order by d.create_time desc
</select> </select>
<select id="selectDeviceShortStatistic" parameterType="com.fastbee.iot.domain.Device" resultType="com.fastbee.iot.model.DeviceStatistic">
select
count(1) as deviceCount,
sum(case when t.status = 3 then 1 else 0 end) as deviceOnlineCount
from (
select d.device_id, d.tenant_id, d.status
from iot_device d
left join iot_product p on p.product_id = d.product_id
<where>
and d.gw_dev_code is null
<if test="entrance != null and (entrance == 1 or entrance == 2)">
and (p.device_type is null or p.device_type != 4)
</if>
<if test="deviceName != null and deviceName != ''"> and d.device_name like concat('%', #{deviceName}, '%')</if>
<if test="productId != null"> and d.product_id = #{productId}</if>
<if test="deviceType != null"> and p.device_type = #{deviceType}</if>
<if test="status != null"> and d.status = #{status}</if>
<if test="deptId != null and deptId != 0 and showChild != null and showChild">
and
( d.tenant_id in (SELECT u.user_id FROM sys_user u
join sys_dept de on u.dept_id = de.dept_id
WHERE FIND_IN_SET(#{deptId}, de.ancestors) OR de.dept_id = #{deptId})
or d.device_id in (select du.device_id from iot_device_share du where du.user_id = #{tenantId})
)
</if>
<if test="deptId != null and deptId != 0 and showChild != null and !showChild">
and (
d.tenant_id = (SELECT dept_user_id FROM sys_dept WHERE dept_id = #{deptId})
or d.device_id in (select du.device_id from iot_device_share du where du.user_id = #{tenantId})
)
</if>
</where>
group by d.device_id, d.tenant_id
) t
</select>
<select id="selectTerminalUserDeviceStatistic" parameterType="com.fastbee.iot.domain.Device" resultType="com.fastbee.iot.model.DeviceStatistic">
select
count(1) as deviceCount,
sum(case when d.status = 3 then 1 else 0 end) as deviceOnlineCount
from (
select device_id from iot_device_user where user_id = #{tenantId}
union
select device_id from iot_device_share where user_id = #{tenantId}
) u
inner join iot_device d on u.device_id = d.device_id
inner join iot_product p on d.product_id = p.product_id
where d.gw_dev_code is null
and (p.device_type is null or p.device_type != 4)
</select>
<select id="selectDeviceByDeviceId" parameterType="Long" resultMap="DeviceResult"> <select id="selectDeviceByDeviceId" parameterType="Long" resultMap="DeviceResult">
select d.device_id, d.device_name, d.product_id, p.product_name,p.device_type, d.tenant_id, d.tenant_name, select d.device_id, d.device_name, d.product_id, p.product_name,p.device_type, d.tenant_id, d.tenant_name,
d.serial_number, d.firmware_version,d.wireless_version, d.status, d.rssi,d.is_shadow,d.is_simulate ,d.location_way,d.things_model_value, d.serial_number, d.firmware_version,d.wireless_version, d.status, d.rssi,d.is_shadow,d.is_simulate ,d.location_way,d.things_model_value,
@ -610,6 +671,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="gwDevCode != null">gw_dev_code,</if> <if test="gwDevCode != null">gw_dev_code,</if>
<if test="isSimulate != null">is_simulate,</if> <if test="isSimulate != null">is_simulate,</if>
<if test="slaveId != null">slave_id,</if> <if test="slaveId != null">slave_id,</if>
<if test="projectId != null">project_id,</if>
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="deviceName != null and deviceName != ''">#{deviceName},</if> <if test="deviceName != null and deviceName != ''">#{deviceName},</if>
@ -640,6 +702,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="gwDevCode != null">#{gwDevCode},</if> <if test="gwDevCode != null">#{gwDevCode},</if>
<if test="isSimulate != null">#{isSimulate},</if> <if test="isSimulate != null">#{isSimulate},</if>
<if test="slaveId != null">#{slaveId},</if> <if test="slaveId != null">#{slaveId},</if>
<if test="projectId != null">#{projectId},</if>
</trim> </trim>
</insert> </insert>
@ -695,6 +758,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="imei != null">imei = #{imei},</if> <if test="imei != null">imei = #{imei},</if>
<if test="iccid != null">iccid = #{iccid},</if> <if test="iccid != null">iccid = #{iccid},</if>
<if test="slaveId != null">slave_id = #{slaveId},</if> <if test="slaveId != null">slave_id = #{slaveId},</if>
<if test="projectId != null">project_id = #{projectId},</if>
</trim> </trim>
where device_id = #{deviceId} where device_id = #{deviceId}
</update> </update>
@ -923,7 +987,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if> <if test="groupId != null and groupId !=0 "> left join iot_device_group g on g.device_id=d.device_id </if>
<where> <where>
and d.gw_dev_code is null and d.gw_dev_code is null
<if test="entrance != null and entrance == 1"> <if test="entrance != null and (entrance == 1 or entrance == 2)">
and (p.device_type is null or p.device_type != 4) and (p.device_type is null or p.device_type != 4)
</if> </if>
<if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if> <if test="groupId != null and groupId !=0 "> and g.group_id = #{groupId}</if>
@ -1077,4 +1141,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where d.product_id = #{productId,jdbcType=BIGINT} where d.product_id = #{productId,jdbcType=BIGINT}
</select> </select>
<!-- 批量更新设备的项目归属 -->
<update id="updateDeviceProjectByIds">
update iot_device set project_id = #{projectId} where device_id in
<foreach item="id" collection="deviceIds" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 清除项目下除指定设备外的其他设备归属 -->
<update id="clearDeviceProjectExcept">
update iot_device set project_id = null where project_id = #{projectId}
<if test="deviceIds != null and deviceIds.length > 0">
and device_id not in
<foreach item="id" collection="deviceIds" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</update>
<update id="clearDeviceProjectByProjectIds">
update iot_device set project_id = null where project_id in
<foreach item="id" collection="projectIds" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper> </mapper>

View File

@ -59,9 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SUM(CASE WHEN d.status = 3 THEN 1 ELSE 0 END) AS online_count, SUM(CASE WHEN d.status = 3 THEN 1 ELSE 0 END) AS online_count,
SUM(CASE WHEN d.device_id IS NOT NULL AND d.status != 3 THEN 1 ELSE 0 END) AS offline_count SUM(CASE WHEN d.device_id IS NOT NULL AND d.status != 3 THEN 1 ELSE 0 END) AS offline_count
FROM iot_project p FROM iot_project p
LEFT JOIN iot_project_group pg ON p.project_id = pg.project_id LEFT JOIN iot_device d ON d.project_id = p.project_id AND d.del_flag = '0'
LEFT JOIN iot_device_group dg ON pg.group_id = dg.group_id
LEFT JOIN iot_device d ON dg.device_id = d.device_id AND d.del_flag = '0'
<where> <where>
p.del_flag = '0' p.del_flag = '0'
<if test="projectName != null and projectName != ''"> and p.project_name like concat('%', #{projectName}, '%')</if> <if test="projectName != null and projectName != ''"> and p.project_name like concat('%', #{projectName}, '%')</if>
@ -79,8 +77,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where project_id = #{projectId} and del_flag = '0' where project_id = #{projectId} and del_flag = '0'
</select> </select>
<select id="selectGroupIdsByProjectId" parameterType="Long" resultType="Long"> <select id="selectDeviceIdsByProjectId" parameterType="Long" resultType="Long">
select group_id from iot_project_group where project_id=#{projectId} select device_id from iot_device where project_id=#{projectId} and del_flag='0'
</select> </select>
<select id="selectProjectsByUserId" parameterType="Long" resultMap="ProjectResult"> <select id="selectProjectsByUserId" parameterType="Long" resultMap="ProjectResult">
@ -102,9 +100,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SUM(CASE WHEN d.status = 3 THEN 1 ELSE 0 END) as onlineDevices, SUM(CASE WHEN d.status = 3 THEN 1 ELSE 0 END) as onlineDevices,
SUM(CASE WHEN d.status != 3 THEN 1 ELSE 0 END) as offlineDevices SUM(CASE WHEN d.status != 3 THEN 1 ELSE 0 END) as offlineDevices
FROM iot_device d FROM iot_device d
INNER JOIN iot_device_group dg ON d.device_id = dg.device_id INNER JOIN iot_project p ON d.project_id = p.project_id
INNER JOIN iot_project_group pg ON dg.group_id = pg.group_id
INNER JOIN iot_project p ON pg.project_id = p.project_id
WHERE p.responsible_user_id = #{userId} WHERE p.responsible_user_id = #{userId}
AND p.del_flag='0' AND p.del_flag='0'
AND d.del_flag='0' AND d.del_flag='0'
@ -114,9 +110,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectProjectDevicesByUserId" parameterType="Long" resultMap="DeviceResult"> <select id="selectProjectDevicesByUserId" parameterType="Long" resultMap="DeviceResult">
SELECT DISTINCT d.device_id, d.device_name, d.serial_number, d.product_id, d.product_name, d.status, d.longitude, d.latitude SELECT DISTINCT d.device_id, d.device_name, d.serial_number, d.product_id, d.product_name, d.status, d.longitude, d.latitude
FROM iot_device d FROM iot_device d
INNER JOIN iot_device_group dg ON d.device_id = dg.device_id INNER JOIN iot_project p ON d.project_id = p.project_id
INNER JOIN iot_project_group pg ON dg.group_id = pg.group_id
INNER JOIN iot_project p ON pg.project_id = p.project_id
WHERE p.responsible_user_id = #{userId} WHERE p.responsible_user_id = #{userId}
AND p.del_flag='0' AND p.del_flag='0'
AND d.del_flag='0' AND d.del_flag='0'
@ -191,15 +185,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</trim> </trim>
</insert> </insert>
<!-- 批量插入项目-分组关系 -->
<insert id="insertProjectGroups">
insert into iot_project_group (project_id,group_id)
values
<foreach item="groupId" collection="groupIds" separator=",">
(#{projectId},#{groupId})
</foreach>
</insert>
<update id="updateProject" parameterType="com.fastbee.iot.domain.Project"> <update id="updateProject" parameterType="com.fastbee.iot.domain.Project">
update iot_project update iot_project
<trim prefix="SET" suffixOverrides=","> <trim prefix="SET" suffixOverrides=",">
@ -226,10 +211,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where project_id = #{projectId} where project_id = #{projectId}
</update> </update>
<delete id="deleteProjectGroupsByProjectId" parameterType="Long">
delete from iot_project_group where project_id=#{projectId}
</delete>
<delete id="deleteProjectByIds" parameterType="String"> <delete id="deleteProjectByIds" parameterType="String">
update iot_project set del_flag = '2' where project_id in update iot_project set del_flag = '2' where project_id in
<foreach item="projectId" collection="array" open="(" separator="," close=")"> <foreach item="projectId" collection="array" open="(" separator="," close=")">
@ -249,9 +230,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SUM(CASE WHEN d.status = 3 THEN 1 ELSE 0 END) as online_count, SUM(CASE WHEN d.status = 3 THEN 1 ELSE 0 END) as online_count,
SUM(CASE WHEN d.status != 3 THEN 1 ELSE 0 END) as offline_count SUM(CASE WHEN d.status != 3 THEN 1 ELSE 0 END) as offline_count
FROM iot_project p FROM iot_project p
LEFT JOIN iot_project_group pg ON p.project_id = pg.project_id LEFT JOIN iot_device d ON d.project_id = p.project_id AND d.del_flag = '0'
LEFT JOIN iot_device_group dg ON pg.group_id = dg.group_id
LEFT JOIN iot_device d ON dg.device_id = d.device_id AND d.del_flag = '0'
WHERE p.project_id = #{projectId} AND p.del_flag = '0' WHERE p.project_id = #{projectId} AND p.del_flag = '0'
GROUP BY p.project_id GROUP BY p.project_id
</select> </select>

View File

@ -0,0 +1,21 @@
-- ========================================
-- FastBee项目设备关联改造迁移脚本
-- 创建日期: 2026-08-01
-- 描述: 将项目-设备关联从两层关联表改为设备表直接外键
-- ========================================
-- 1. 设备表新增所属项目字段
ALTER TABLE `iot_device`
ADD COLUMN `project_id` BIGINT(20) DEFAULT NULL COMMENT '所属项目ID' AFTER `tenant_name`,
ADD INDEX `idx_project_id` (`project_id`);
-- 2. 废弃项目-分组关系表(未上线,无需迁移数据)
DROP TABLE IF EXISTS `iot_project_group`;
-- ========================================
-- 变更说明:
-- 1. 项目与设备的关系从"项目→分组→设备"两层间接关联,简化为"项目→设备"直接关联
-- 2. 一个设备最多属于一个项目(1:1),一个项目可包含多个设备(1:N)
-- 3. iot_device.project_id 为 NULL 表示设备未关联项目
-- 4. iot_group / iot_device_group 表保留,继续用于独立的设备分组管理功能
-- ========================================

View File

@ -1,10 +1,11 @@
-- ======================================== -- ========================================
-- FastBee项目管理数据库表创建脚本 -- FastBee项目管理数据库表创建脚本
-- 创建日期: 2026-06-28 -- 创建日期: 2026-06-28
-- 描述: 创建项目表和项目-分组关系表,实现User→Project→Group→Device层级 -- 更新日期: 2026-08-01
-- 描述: 创建项目表,实现User→Project→Device直接关联
-- ======================================== -- ========================================
-- 1. 创建项目表(iot_project) -- 创建项目表(iot_project)
-- 实现 User → Project 层级(通过responsible_user_id字段) -- 实现 User → Project 层级(通过responsible_user_id字段)
CREATE TABLE `iot_project` ( CREATE TABLE `iot_project` (
`project_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '项目ID', `project_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '项目ID',
@ -33,34 +34,26 @@ CREATE TABLE `iot_project` (
KEY `idx_create_time` (`create_time`) KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='业务项目表'; ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='业务项目表';
-- 2. 创建项目-分组关系表(iot_project_group)
-- 实现 Project → Device Group 层级
CREATE TABLE `iot_project_group` (
`project_id` BIGINT(20) NOT NULL COMMENT '项目ID',
`group_id` BIGINT(20) NOT NULL COMMENT '设备分组ID',
PRIMARY KEY (`project_id`, `group_id`),
KEY `idx_project_id` (`project_id`),
KEY `idx_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='项目-设备分组关系表';
-- ======================================== -- ========================================
-- 层级关系说明: -- 层级关系说明:
-- User (responsible_user_id) → Project (project_id) → Project_Group (group_id) → Device_Group (device_id) → Device -- User (responsible_user_id) → Project (project_id) → Device (project_id)
--
-- 项目与设备的关系:
-- - 一个设备最多属于一个项目(通过 iot_device.project_id 外键)
-- - 一个项目可以包含多个设备
-- - iot_device.project_id 为 NULL 表示设备未关联项目
-- --
-- 与现有逻辑兼容的关键点: -- 与现有逻辑兼容的关键点:
-- 1. iot_group表保持不变(现有分组管理逻辑不受影响) -- 1. iot_group / iot_device_group 表保持不变(独立的设备分组管理功能)
-- 2. iot_device_user表保持不变(设备归属关系独立) -- 2. iot_device_user 表保持不变(设备归属关系独立)
-- 3. 项目-分组关系是可选的(未关联项目的分组保持现有功能)
-- --
-- 用户查询设备的两种路径: -- 用户查询设备的两种路径:
-- 路径1:查询用户负责的项目设备(新增功能) -- 路径1:查询用户负责的项目设备(项目维度)
-- SELECT DISTINCT d.* FROM iot_device d -- SELECT d.* FROM iot_device d
-- INNER JOIN iot_device_group dg ON d.device_id = dg.device_id -- INNER JOIN iot_project p ON d.project_id = p.project_id
-- INNER JOIN iot_project_group pg ON dg.group_id = pg.group_id
-- INNER JOIN iot_project p ON pg.project_id = p.project_id
-- WHERE p.responsible_user_id = #{userId} AND p.del_flag='0' AND d.del_flag='0' -- WHERE p.responsible_user_id = #{userId} AND p.del_flag='0' AND d.del_flag='0'
-- --
-- 路径2:查询设备归属用户的设备(现有逻辑) -- 路径2:查询设备归属用户的设备(用户维度)
-- SELECT d.* FROM iot_device d -- SELECT d.* FROM iot_device d
-- INNER JOIN iot_device_user du ON d.device_id = du.device_id -- INNER JOIN iot_device_user du ON d.device_id = du.device_id
-- WHERE du.user_id = #{userId} AND d.del_flag='0' -- WHERE du.user_id = #{userId} AND d.del_flag='0'

View File

@ -36,6 +36,15 @@ export function listDeviceShort(query) {
}); });
} }
// 查询当前用户可见设备统计(大屏首页)
export function getDeviceShortStatistic(query) {
return request({
url: '/iot/device/userShortStatistic',
method: 'get',
params: query,
});
}
// 查询所有设备简短列表 // 查询所有设备简短列表
export function listAllDeviceShort(query) { export function listAllDeviceShort(query) {
return request({ return request({

View File

@ -17,14 +17,24 @@ export function getProject(projectId) {
}) })
} }
// 查询项目下的关联分组ID数组 // 查询项目下的关联设备ID数组
export function getGroupIds(projectId) { export function getProjectDeviceIds(projectId) {
return request({ return request({
url: '/iot/project/getGroupIds/' + projectId, url: '/iot/project/deviceIds/' + projectId,
method: 'get' method: 'get'
}) })
} }
// 更新项目下的关联设备
export function updateProjectDevices(projectId, deviceIds) {
return request({
url: '/iot/project/updateProjectDevices',
method: 'put',
params: { projectId },
data: deviceIds
})
}
// 新增项目 // 新增项目
export function addProject(data) { export function addProject(data) {
return request({ return request({
@ -43,16 +53,6 @@ export function updateProject(data) {
}) })
} }
// 更新项目下的分组关联
export function updateProjectGroups(projectId, groupIds) {
return request({
url: '/iot/project/updateProjectGroups',
method: 'put',
params: { projectId },
data: groupIds
})
}
// 删除项目 // 删除项目
export function delProject(projectId) { export function delProject(projectId) {
return request({ return request({

View File

@ -1,10 +1,11 @@
const DEFAULT_TIANDITU_AK = process.env.VUE_APP_TIANDITU_AK || 'c9ef88370bacf8f51662c01ad80a580c'; const DEFAULT_TIANDITU_AK = process.env.VUE_APP_TIANDITU_AK || 'c9ef88370bacf8f51662c01ad80a580c';
const DEFAULT_TIANDITU_TILE_KEY = process.env.VUE_APP_TIANDITU_TILE_KEY || 'b920e6076a06aa279c974f5f76cf746e'; // 瓦片 key 必须是“浏览器端”类型;旧默认值 b920... 是服务器端 key,浏览器请求会返回 403(权限类型错误)
const DEFAULT_TIANDITU_TILE_KEY = process.env.VUE_APP_TIANDITU_TILE_KEY || 'c9ef88370bacf8f51662c01ad80a580c';
let tiandituLoading = null; let tiandituLoading = null;
export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) { export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) {
if (window.T) { if (window.T && window.T.Map) {
return Promise.resolve(window.T); return Promise.resolve(window.T);
} }
if (tiandituLoading) { if (tiandituLoading) {
@ -12,11 +13,10 @@ export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) {
} }
tiandituLoading = new Promise((resolve, reject) => { tiandituLoading = new Promise((resolve, reject) => {
// 移除之前可能加载失败的脚本,避免复用失效的 script 标签
const existingScript = document.getElementById('tianditu-js-api'); const existingScript = document.getElementById('tianditu-js-api');
if (existingScript) { if (existingScript) {
existingScript.addEventListener('load', () => resolve(window.T)); existingScript.remove();
existingScript.addEventListener('error', reject);
return;
} }
const script = document.createElement('script'); const script = document.createElement('script');
@ -25,14 +25,18 @@ export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) {
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:'; const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
script.src = `${protocol}//api.tianditu.gov.cn/api?v=4.0&tk=${tk}`; script.src = `${protocol}//api.tianditu.gov.cn/api?v=4.0&tk=${tk}`;
script.onload = () => { script.onload = () => {
if (window.T) { if (window.T && window.T.Map) {
resolve(window.T); resolve(window.T);
} else { } else {
reject(new Error('天地图脚本已加载,但未找到 T 对象')); reject(new Error('天地图脚本已加载,但未找到 T 对象'));
} }
}; };
script.onerror = reject; script.onerror = (err) => reject(err);
document.head.appendChild(script); document.head.appendChild(script);
}).catch((err) => {
// 加载失败时清空缓存,允许下次重试
tiandituLoading = null;
throw err;
}); });
return tiandituLoading; return tiandituLoading;

View File

@ -16,7 +16,7 @@
.cockpit-page { .cockpit-page {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: #020918; background: #010820;
} }
.cockpit-page--map-view .cockpit-bg { .cockpit-page--map-view .cockpit-bg {
@ -24,69 +24,8 @@
background: #05070b; background: #05070b;
} }
.cockpit-page--map-view .cockpit-header--new {
height: 54px;
margin-bottom: 0;
background: rgba(31, 36, 42, .96);
border-bottom: 1px solid rgba(0, 0, 0, .6);
}
.cockpit-page--map-view .header-title-console {
top: 0;
width: 360px;
height: 54px;
background: transparent;
clip-path: none;
border: 0;
box-shadow: none;
}
.cockpit-page--map-view .header-title-console::before,
.cockpit-page--map-view .header-title-console::after {
display: none;
}
.cockpit-page--map-view .header-title-console__text {
color: #d7dde5;
font-size: 18px;
letter-spacing: 1px;
text-shadow: none;
}
.cockpit-page--map-view .header-wing {
top: 0;
width: 180px;
height: 54px;
padding: 0;
color: #bfc7d0;
font-size: 18px;
background: transparent;
clip-path: none;
border: 0;
box-shadow: none;
}
.cockpit-page--map-view .header-wing i {
color: #1ba6e8;
font-size: 22px;
}
.cockpit-page--map-view .header-wing--left {
left: 0;
}
.cockpit-page--map-view .header-wing--right {
left: 184px;
right: auto;
}
.cockpit-page--map-view .admin-stealth {
top: 10px;
right: 18px;
}
.cockpit-page--map-view .cockpit-main { .cockpit-page--map-view .cockpit-main {
height: calc(100% - 54px); height: calc(100% - 84px);
} }
.cockpit-bg { .cockpit-bg {
@ -98,7 +37,7 @@
background: background:
radial-gradient(circle at 50% 12%, rgba(45, 120, 255, .42), transparent 30%), radial-gradient(circle at 50% 12%, rgba(45, 120, 255, .42), transparent 30%),
radial-gradient(circle at 12% 88%, rgba(0, 242, 255, .14), transparent 28%), radial-gradient(circle at 12% 88%, rgba(0, 242, 255, .14), transparent 28%),
linear-gradient(135deg, #030814 0%, #071a46 42%, #030918 100%); linear-gradient(135deg, #010820 0%, #092156 42%, #010820 100%);
overflow: hidden; overflow: hidden;
&::before { &::before {
@ -120,125 +59,319 @@
height: 100%; height: 100%;
} }
.cockpit-header--new { .cockpit-header {
position: relative; position: relative;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
height: 76px; height: 76px;
margin-bottom: 8px; margin-bottom: 8px;
padding: 0 24px;
box-sizing: border-box;
background:
linear-gradient(180deg, rgba(14, 42, 116, .9), rgba(6, 20, 64, .92)),
radial-gradient(circle at 50% -60%, rgba(67, 225, 255, .25), transparent 52%);
border-bottom: 1px solid rgba(69, 204, 255, .32);
box-shadow: inset 0 -14px 24px rgba(0, 7, 52, .3), 0 6px 22px rgba(0, 10, 50, .35);
&::after {
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 2px;
content: '';
background: linear-gradient(90deg, transparent, rgba(94, 232, 255, .85), transparent);
}
} }
.header-title-console { // 左侧模式导航
.header-nav {
display: flex;
align-items: center;
gap: 8px;
z-index: 2;
&__item {
display: flex;
align-items: center;
gap: 7px;
height: 38px;
padding: 0 18px;
border: 1px solid rgba(99, 184, 255, .28);
border-radius: 4px;
color: #a9cef2;
font-size: 15px;
font-weight: 600;
letter-spacing: 1px;
background: rgba(4, 16, 56, .3);
cursor: pointer;
transition: all .18s ease;
i {
color: #3ec9ff;
font-size: 17px;
}
&:hover {
color: #eaf8ff;
border-color: rgba(104, 224, 255, .6);
background: rgba(12, 45, 110, .55);
}
&.active {
color: #fff;
border-color: rgba(69, 204, 255, .65);
background: linear-gradient(180deg, rgba(24, 96, 200, .85), rgba(10, 46, 120, .85));
box-shadow: 0 0 12px rgba(32, 150, 255, .3);
i {
color: #65efff;
}
}
}
}
// 中央标题
.header-center {
position: absolute; position: absolute;
top: -10px;
left: 50%; left: 50%;
z-index: 4; top: 50%;
width: 580px; z-index: 3;
height: 72px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 62px;
padding: 0 52px;
transform: translate(-50%, -50%);
cursor: pointer;
background:
linear-gradient(180deg, rgba(28, 75, 202, .92), rgba(9, 33, 86, .92) 70%, rgba(5, 14, 56, .92)),
radial-gradient(circle at 50% -20%, rgba(67, 225, 255, .3), transparent 58%);
clip-path: polygon(6% 0, 94% 0, 100% 55%, 94% 100%, 6% 100%, 0 55%);
border-bottom: 2px solid rgba(36, 198, 255, .75);
box-shadow: 0 14px 30px rgba(0, 20, 90, .5), inset 0 -10px 20px rgba(0, 7, 52, .3);
&__inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
pointer-events: none;
}
&__cn {
color: #f7fbff; color: #f7fbff;
cursor: pointer; font-size: 26px;
background:
linear-gradient(180deg, rgba(28, 75, 202, .98), rgba(7, 21, 83, .98) 70%, rgba(5, 14, 56, .98)),
radial-gradient(circle at 50% -10%, rgba(67, 225, 255, .28), transparent 58%);
clip-path: polygon(8% 0, 92% 0, 100% 82%, 86% 100%, 14% 100%, 0 82%);
border-bottom: 2px solid rgba(36, 198, 255, .82);
box-shadow: 0 16px 34px rgba(0, 20, 90, .56), inset 0 -12px 22px rgba(0, 7, 52, .34);
transform: translateX(-50%);
&::before,
&::after {
position: absolute;
content: '';
}
&::before {
left: 34px;
right: 34px;
bottom: 8px;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(94, 232, 255, .9), transparent);
}
&::after {
inset: 8px 40px auto;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(125, 222, 255, .7), transparent);
}
}
.header-title-console__text {
font-size: 34px;
font-weight: 900; font-weight: 900;
letter-spacing: 4px; letter-spacing: 5px;
line-height: 1; line-height: 1;
white-space: nowrap;
text-shadow: 0 0 18px rgba(87, 220, 255, .68); text-shadow: 0 0 18px rgba(87, 220, 255, .68);
}
&__en {
color: rgba(161, 212, 255, .75);
font-size: 10px;
letter-spacing: 3px;
text-transform: uppercase;
white-space: nowrap;
}
} }
.header-wing { // 右侧时间 + 用户
position: absolute; .header-aside {
top: 0; display: flex;
z-index: 3; align-items: center;
width: 500px; gap: 20px;
height: 60px; z-index: 2;
border: 0; }
color: #eef8ff;
cursor: pointer; .header-clock {
font-size: 27px; display: flex;
font-weight: 800; align-items: baseline;
gap: 10px;
&__date {
color: #b8d9ff;
font-size: 13px;
letter-spacing: 1px;
}
&__time {
color: #eff7ff;
font-size: 22px;
font-weight: 900;
letter-spacing: 2px; letter-spacing: 2px;
background: text-shadow: 0 0 12px rgba(86, 214, 255, .5);
linear-gradient(180deg, rgba(28, 68, 178, .92), rgba(9, 28, 93, .88)), }
linear-gradient(90deg, transparent, rgba(54, 187, 255, .16), transparent);
border-bottom: 1px solid rgba(69, 204, 255, .5); &__week {
box-shadow: inset 0 -12px 20px rgba(5, 14, 66, .34); color: #a6bde8;
transition: color .18s ease, filter .18s ease, transform .18s ease; font-size: 13px;
}
} }
.header-wing i { .header-user {
margin-right: 12px; display: flex;
color: #2ecbff; align-items: center;
font-size: 32px;
vertical-align: -3px;
}
.header-wing:hover, &__trigger {
.header-wing.active { display: flex;
color: #fff; align-items: center;
filter: brightness(1.12); gap: 10px;
transform: translateY(-1px); height: 44px;
} padding: 0 14px 0 5px;
border: 1px solid rgba(99, 184, 255, .3);
border-radius: 22px;
background: rgba(4, 16, 56, .4);
cursor: pointer;
transition: all .18s ease;
.header-wing--left { &:hover {
left: 182px; border-color: rgba(104, 224, 255, .6);
padding-right: 132px; background: rgba(12, 45, 110, .55);
clip-path: polygon(0 0, 88% 0, 100% 100%, 8% 100%); }
} }
.header-wing--right { &__avatar {
right: 182px;
padding-left: 132px;
clip-path: polygon(12% 0, 100% 0, 92% 100%, 0 100%);
}
.admin-stealth {
position: absolute;
top: 12px;
right: 22px;
z-index: 8;
width: 34px; width: 34px;
height: 34px; height: 34px;
border: 1px solid rgba(99, 184, 255, .26); border-radius: 50%;
color: rgba(161, 212, 255, .46); object-fit: cover;
cursor: pointer; border: 2px solid rgba(69, 204, 255, .6);
background: rgba(4, 16, 56, .3); box-shadow: 0 0 10px rgba(32, 150, 255, .4);
transition: color .18s ease, border-color .18s ease, background .18s ease; }
&__name {
max-width: 120px;
overflow: hidden;
color: #eaf8ff;
font-size: 14px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
&__arrow {
color: rgba(161, 212, 255, .6);
font-size: 12px;
}
} }
.admin-stealth:hover { // 下拉菜单样式(全局样式,通过 popper-class 指定)
.admin-dropdown-popper {
position: fixed !important;
z-index: 9999 !important;
background: rgba(6, 22, 68, 0.98) !important;
border: 1px solid rgba(69, 204, 255, 0.32) !important;
box-shadow: 0 8px 24px rgba(0, 10, 40, 0.6) !important;
min-width: 140px !important;
padding: 6px 0 !important;
margin-top: 8px !important;
.el-dropdown-menu__item {
color: #b8d9ff !important;
font-size: 14px !important;
padding: 10px 18px !important;
line-height: 1.4 !important;
transition: all 0.2s ease !important;
i {
margin-right: 8px;
color: #5ec8ff;
}
&:hover {
background: rgba(16, 52, 128, 0.72) !important;
color: #8fe8ff !important;
}
}
.popper__arrow {
border-bottom-color: rgba(69, 204, 255, 0.32) !important;
}
.popper__arrow::after {
border-bottom-color: rgba(6, 22, 68, 0.98) !important;
}
}
// 退出登录确认框(科技风,通过 customClass 指定)
.tech-confirm-box {
background: linear-gradient(180deg, rgba(9, 30, 82, 0.98), rgba(4, 12, 44, 0.98)) !important;
border: 1px solid rgba(69, 204, 255, 0.4) !important;
border-radius: 8px !important;
box-shadow: 0 18px 50px rgba(0, 8, 36, 0.85), 0 0 26px rgba(32, 150, 255, 0.18) !important;
.el-message-box__header {
padding: 16px 20px 8px !important;
border-bottom: 1px solid rgba(69, 204, 255, 0.2) !important;
}
.el-message-box__title {
color: #dff7ff !important;
font-weight: 700;
letter-spacing: 1px;
display: flex;
align-items: center;
gap: 6px;
&::before {
content: '\e6db';
font-family: 'element-icons';
color: #ff5574;
}
}
.el-message-box__headerbtn .el-message-box__close {
color: rgba(161, 212, 255, 0.6) !important;
&:hover {
color: #8fe8ff !important;
}
}
.el-message-box__content {
padding: 22px 20px 8px !important;
}
.el-message-box__message {
color: #b8d9ff !important;
font-size: 14px !important;
line-height: 1.6 !important;
}
.el-message-box__btns {
padding: 14px 20px 18px !important;
display: flex;
justify-content: flex-end;
gap: 10px;
.el-button {
border-color: rgba(99, 184, 255, 0.3);
color: #b8d9ff;
background: rgba(4, 16, 56, 0.3);
&:hover {
color: #8fe8ff; color: #8fe8ff;
border-color: rgba(104, 224, 255, .64); border-color: rgba(104, 224, 255, 0.64);
background: rgba(7, 35, 95, .72); background: rgba(7, 35, 95, 0.72);
}
}
.el-button--primary {
border-color: rgba(255, 85, 116, 0.7);
color: #fff;
background: linear-gradient(180deg, rgba(200, 45, 80, 0.92), rgba(140, 20, 55, 0.92));
&:hover {
border-color: #ff5574;
background: linear-gradient(180deg, rgba(230, 60, 100, 0.95), rgba(160, 30, 70, 0.95));
}
}
}
} }
.map-time-overlay { .map-time-overlay {
@ -306,7 +439,7 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(46, 152, 255, .46); border: 1px solid rgba(46, 152, 255, .46);
background: background:
linear-gradient(180deg, rgba(11, 42, 117, .82), rgba(5, 18, 58, .72)), linear-gradient(180deg, rgba(9, 33, 86, .82), rgba(5, 18, 58, .72)),
radial-gradient(circle at 100% 0, rgba(0, 209, 255, .2), transparent 38%); radial-gradient(circle at 100% 0, rgba(0, 209, 255, .2), transparent 38%);
box-shadow: box-shadow:
inset 0 0 28px rgba(28, 142, 255, .18), inset 0 0 28px rgba(28, 142, 255, .18),
@ -353,7 +486,7 @@
margin-right: 8px; margin-right: 8px;
content: ''; content: '';
border-radius: 6px; border-radius: 6px;
background: linear-gradient(180deg, #65f5ff, #1477ff); background: linear-gradient(180deg, #65f5ff, #1A5CD7);
box-shadow: 0 0 10px rgba(63, 231, 255, .8); box-shadow: 0 0 10px rgba(63, 231, 255, .8);
} }
} }
@ -380,7 +513,7 @@
padding: 12px 8px; padding: 12px 8px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(75, 188, 255, .2); border: 1px solid rgba(75, 188, 255, .2);
background: linear-gradient(180deg, rgba(18, 74, 157, .62), rgba(6, 27, 74, .78)); background: linear-gradient(180deg, rgba(9, 33, 86, .62), rgba(6, 27, 74, .78));
} }
.metric-label { .metric-label {
@ -422,7 +555,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid rgba(42, 181, 255, .26); border: 1px solid rgba(42, 181, 255, .26);
background: rgba(8, 35, 92, .54); background: rgba(9, 33, 86, .54);
} }
} }
@ -543,7 +676,7 @@
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
border: 1px solid rgba(44, 158, 255, .4); border: 1px solid rgba(44, 158, 255, .4);
background: rgba(3, 13, 37, .62); background: rgba(1, 8, 32, .62);
box-shadow: inset 0 0 48px rgba(28, 128, 255, .16); box-shadow: inset 0 0 48px rgba(28, 128, 255, .16);
overflow: hidden; overflow: hidden;
} }
@ -619,6 +752,32 @@
font-size: 12px; font-size: 12px;
border: 1px solid rgba(62, 179, 255, .3); border: 1px solid rgba(62, 179, 255, .3);
background: rgba(3, 17, 50, .72); background: rgba(3, 17, 50, .72);
pointer-events: auto;
}
.map-back-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
color: #fff;
font-size: 14px;
background: linear-gradient(135deg, rgba(24, 144, 255, .9), rgba(0, 200, 255, .8));
border: 1px solid rgba(94, 219, 255, .5);
border-radius: 4px;
cursor: pointer;
transition: all .3s;
pointer-events: auto;
&:hover {
background: linear-gradient(135deg, rgba(24, 144, 255, 1), rgba(0, 200, 255, 1));
box-shadow: 0 0 12px rgba(0, 200, 255, .6);
transform: translateY(-2px);
}
i {
font-size: 16px;
}
} }
.map-legends--view { .map-legends--view {
@ -686,7 +845,7 @@
background: background:
radial-gradient(circle at 50% 52%, rgba(24, 155, 255, .28), transparent 40%), radial-gradient(circle at 50% 52%, rgba(24, 155, 255, .28), transparent 40%),
radial-gradient(circle at 50% 50%, rgba(5, 31, 80, .7), transparent 62%), radial-gradient(circle at 50% 50%, rgba(5, 31, 80, .7), transparent 62%),
linear-gradient(180deg, rgba(3, 15, 44, .78), rgba(2, 9, 28, .96)); linear-gradient(180deg, rgba(1, 8, 32, .78), rgba(1, 8, 32, .96));
} }
.china-map { .china-map {
@ -704,7 +863,7 @@
color: #9ec9e8; color: #9ec9e8;
font-size: 12px; font-size: 12px;
border: 1px solid rgba(70, 195, 255, .28); border: 1px solid rgba(70, 195, 255, .28);
background: rgba(3, 18, 56, .72); background: rgba(1, 8, 32, .72);
} }
.selected-project { .selected-project {
@ -715,8 +874,9 @@
min-width: 280px; min-width: 280px;
padding: 14px 16px; padding: 14px 16px;
border: 1px solid rgba(70, 195, 255, .34); border: 1px solid rgba(70, 195, 255, .34);
background: rgba(3, 18, 56, .78); background: rgba(1, 8, 32, .78);
box-shadow: 0 12px 30px rgba(0, 0, 0, .34); box-shadow: 0 12px 30px rgba(0, 0, 0, .34);
pointer-events: none;
} }
.selected-project--view { .selected-project--view {
@ -796,6 +956,12 @@
cursor: pointer; cursor: pointer;
} }
.project-row--active {
border-color: rgba(72, 218, 255, .95);
background: rgba(14, 59, 126, .9);
box-shadow: inset 0 0 0 1px rgba(72, 218, 255, .35), 0 0 12px rgba(72, 218, 255, .4);
}
.project-main, .project-main,
.project-meta { .project-meta {
display: flex; display: flex;
@ -907,7 +1073,7 @@
th.el-table__cell { th.el-table__cell {
color: #bde2ff; color: #bde2ff;
background: rgba(9, 39, 93, .86); background: rgba(9, 33, 86, .86);
border-color: rgba(82, 168, 255, .2); border-color: rgba(82, 168, 255, .2);
} }
@ -930,3 +1096,58 @@
top: 0; top: 0;
left: 0; left: 0;
} }
/* 人员设备视图:加载更多按钮 */
.device-panel {
display: flex;
flex-direction: column;
.device-list {
flex: 1;
min-height: 0;
height: auto;
overflow: auto;
}
.device-load-more {
flex: none;
display: flex;
align-items: center;
justify-content: center;
height: 30px;
margin-top: 8px;
border: 1px solid rgba(71, 190, 255, .3);
border-radius: 4px;
color: #9ec9e8;
font-size: 12px;
letter-spacing: 1px;
cursor: pointer;
background: rgba(6, 22, 62, .5);
transition: all .18s ease;
user-select: none;
&:hover:not(.is-disabled) {
color: #8fe8ff;
border-color: rgba(104, 224, 255, .6);
background: rgba(14, 59, 126, .62);
}
&.is-disabled {
opacity: .45;
cursor: not-allowed;
}
}
}
/* 大屏日期样式 */
.header-clock__date {
font-size: 26px; /* 日期 */
}
.header-clock__time {
font-size: 38px; /* 时间,加粗显示,调大一些 */
font-weight: bold;
}
.header-clock__week {
font-size: 26px; /* 星期 */
}

View File

@ -3,21 +3,60 @@
<div class="cockpit-bg"> <div class="cockpit-bg">
<dv-loading v-if="loading">Loading...</dv-loading> <dv-loading v-if="loading">Loading...</dv-loading>
<div v-else class="cockpit-shell"> <div v-else class="cockpit-shell">
<header class="cockpit-header--new"> <header class="cockpit-header">
<button type="button" class="header-wing header-wing--left" :class="{ active: activeMode === 'mapView' }" @click.stop="switchMode('mapView')"> <!-- 左侧模式切换导航 -->
<nav class="header-nav">
<button type="button" class="header-nav__item" :class="{ active: activeMode === 'mapView' }" @click.stop="switchMode('mapView')">
<i class="el-icon-location-information"></i> <i class="el-icon-location-information"></i>
<span>地图模式</span> <span>地图模式</span>
</button> </button>
<div class="header-title-console" :class="{ active: activeMode === 'screen' }" @click.stop="goHome"> <button type="button" class="header-nav__item" :class="{ active: activeMode === 'screen' }" @click.stop="goHome">
<div class="header-title-console__text">岩安测控大数据中心</div> <i class="el-icon-data-line"></i>
</div> <span>综合大屏</span>
<button type="button" class="header-wing header-wing--right" :class="{ active: activeMode === 'analysis' }" @click.stop="switchMode('analysis')"> </button>
<button type="button" class="header-nav__item" :class="{ active: activeMode === 'analysis' }" @click.stop="switchMode('analysis')">
<i class="el-icon-data-analysis"></i> <i class="el-icon-data-analysis"></i>
<span>分析模式</span> <span>分析模式</span>
</button> </button>
<button type="button" class="admin-stealth" title="管理后台" @click.stop="goAdmin"> </nav>
<i class="el-icon-setting"></i>
</button> <!-- 中央标题 -->
<div class="header-center" @click.stop="goHome">
<div class="header-center__inner">
<span class="header-center__cn">岩安测控大数据中心</span>
<span class="header-center__en">YAN'AN IOT BIG DATA CENTER</span>
</div>
</div>
<!-- 右侧时间 + 用户 -->
<div class="header-aside">
<div class="header-clock">
<span class="header-clock__date">{{ dateYear }}</span>
<strong class="header-clock__time">{{ dateDay }}</strong>
<span class="header-clock__week">{{ dateWeek }}</span>
</div>
<el-dropdown class="header-user" popper-class="admin-dropdown-popper" trigger="click" @command="handleAdminCommand">
<div class="header-user__trigger" title="更多操作">
<img class="header-user__avatar" :src="headerAvatar" alt="avatar" />
<span class="header-user__name">{{ headerUserName }}</span>
<i class="el-icon-arrow-down header-user__arrow"></i>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="profile" icon="el-icon-user">
<span>个人中心</span>
</el-dropdown-item>
<el-dropdown-item command="pwd" icon="el-icon-key" divided>
<span>修改密码</span>
</el-dropdown-item>
<el-dropdown-item command="admin" icon="el-icon-monitor" divided>
<span>管理后台</span>
</el-dropdown-item>
<el-dropdown-item command="logout" icon="el-icon-switch-button" divided>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</header> </header>
<main v-show="activeMode === 'screen'" class="cockpit-main cockpit-main--map"> <main v-show="activeMode === 'screen'" class="cockpit-main cockpit-main--map">
@ -32,23 +71,13 @@
</div> </div>
</div> </div>
<div class="panel-card"> <div class="panel-card device-panel">
<div class="panel-title panel-title--between"> <div class="panel-title panel-title--between">
<span>人员设备视图</span> <span>人员设备视图</span>
<span class="panel-subtitle">当前用户</span> <span class="panel-subtitle">当前用户</span>
</div> </div>
<div class="user-device-summary">
<div>
<span class="summary-number">{{ userDeviceCount }}</span>
<span class="summary-label">关联设备</span>
</div>
<div>
<span class="summary-number summary-number--warn">{{ alarmCount }}</span>
<span class="summary-label">告警数量</span>
</div>
</div>
<div class="device-list"> <div class="device-list">
<div v-for="device in userDevicePreview" :key="device.deviceId" class="device-row" @click="focusDevice(device)"> <div v-for="device in userDevicePreview" :key="device.deviceId" class="device-row" @click="focusDeviceAnalysis(device)">
<span class="device-dot" :class="{ online: Number(device.status) === 3 }"></span> <span class="device-dot" :class="{ online: Number(device.status) === 3 }"></span>
<div class="device-info"> <div class="device-info">
<div class="device-name">{{ device.deviceName || device.serialNumber }}</div> <div class="device-name">{{ device.deviceName || device.serialNumber }}</div>
@ -58,13 +87,17 @@
</div> </div>
<div v-if="userDevicePreview.length === 0" class="empty-tip">暂无关联设备</div> <div v-if="userDevicePreview.length === 0" class="empty-tip">暂无关联设备</div>
</div> </div>
<div v-if="showDeviceLoadMore" class="device-load-more" :class="{ 'is-disabled': deviceLoadingMore }"
@click="selectedProject ? loadMoreProjectDevices() : loadMoreUserDevices()">
{{ deviceLoadingMore ? '加载中...' : '加载更多' }}
</div>
</div> </div>
<div class="panel-card"> <div class="panel-card">
<div class="panel-title">滚动预警</div> <div class="panel-title">滚动预警</div>
<div class="alarm-list"> <div class="alarm-list">
<vue-seamless-scroll v-if="alarmList.length" :data="alarmList" :class-option="scrollOption"> <vue-seamless-scroll v-if="alarmList.length" :data="alarmList" :class-option="scrollOption">
<div v-for="(alarm, index) in alarmList" :key="alarm.alertLogId || alarm.id || index" class="alarm-row"> <div v-for="(alarm, index) in activeAlarmList" :key="alarm.alertLogId || alarm.id || index" class="alarm-row">
<span class="alarm-index">{{ index + 1 }}</span> <span class="alarm-index">{{ index + 1 }}</span>
<div class="alarm-content"> <div class="alarm-content">
<div class="alarm-name">{{ alarm.deviceName || alarm.serialNumber || '未知设备' }}</div> <div class="alarm-name">{{ alarm.deviceName || alarm.serialNumber || '未知设备' }}</div>
@ -78,11 +111,12 @@
</section> </section>
<section class="map-stage"> <section class="map-stage">
<!-- 大屏时间
<div class="map-time-overlay"> <div class="map-time-overlay">
<span>{{ dateYear }}</span> <span>{{ dateYear }}</span>
<span>{{ dateWeek }}</span> <span>{{ dateWeek }}</span>
<strong>{{ dateDay }}</strong> <strong>{{ dateDay }}</strong>
</div> </div> -->
<div class="map-toolbar"> <div class="map-toolbar">
<div> <div>
<div class="map-title">项目 / 设备分布</div> <div class="map-title">项目 / 设备分布</div>
@ -93,6 +127,10 @@
<span><i class="legend-dot legend-dot--running"></i>运行项目</span> <span><i class="legend-dot legend-dot--running"></i>运行项目</span>
<span><i class="legend-dot legend-dot--completed"></i>完结项目</span> <span><i class="legend-dot legend-dot--completed"></i>完结项目</span>
</div> </div>
<button v-if="currentProvince" type="button" class="map-back-btn" @click="backToChina">
<i class="el-icon-back"></i>
<span>返回全国</span>
</button>
</div> </div>
<div class="china-map-wrap"> <div class="china-map-wrap">
<div ref="dashboardChinaMap" class="china-map"></div> <div ref="dashboardChinaMap" class="china-map"></div>
@ -125,7 +163,13 @@
<span class="panel-subtitle">{{ projectList.length }} 个</span> <span class="panel-subtitle">{{ projectList.length }} 个</span>
</div> </div>
<div class="project-list"> <div class="project-list">
<div v-for="project in projectPreview" :key="project.id" class="project-row" @click="selectProject(project)"> <div
v-for="project in projectPreview"
:key="project.id"
class="project-row"
:class="{ 'project-row--active': isProjectSelected(project) }"
@click="handleProjectRowClick(project)"
>
<div class="project-main"> <div class="project-main">
<span class="project-status" :class="'project-status--' + project.status"></span> <span class="project-status" :class="'project-status--' + project.status"></span>
<span>{{ project.name }}</span> <span>{{ project.name }}</span>
@ -184,9 +228,11 @@
<div class="panel-title">分析条件</div> <div class="panel-title">分析条件</div>
<el-form label-position="top" size="mini" class="analysis-form"> <el-form label-position="top" size="mini" class="analysis-form">
<el-form-item label="设备"> <el-form-item label="设备">
<el-select v-model="analysisQuery.deviceId" filterable placeholder="请选择设备" @change="handleAnalysisDeviceChange"> <el-select v-model="analysisQuery.deviceId" filterable remote clearable placeholder="请选择设备(支持搜索)"
:remote-method="searchAnalysisDevices" :loading="analysisDeviceSearchLoading"
@visible-change="onAnalysisDeviceVisible" @change="handleAnalysisDeviceChange">
<el-option <el-option
v-for="device in deviceList" v-for="device in analysisDeviceOptions"
:key="device.deviceId" :key="device.deviceId"
:label="device.deviceName || device.serialNumber" :label="device.deviceName || device.serialNumber"
:value="device.deviceId" :value="device.deviceId"
@ -209,7 +255,11 @@
<div class="panel-card analysis-card"> <div class="panel-card analysis-card">
<div class="panel-title">测点 / 物模型</div> <div class="panel-title">测点 / 物模型</div>
<div class="model-list"> <div class="model-list">
<div v-for="model in identifierList" :key="model.identifier" class="model-row"> <div
v-for="(model, index) in identifierList"
:key="'model-' + model.modelId + '-' + (model.serialNumber || '') + '-' + index"
class="model-row"
>
<span>{{ model.modelName || model.identifier }}</span> <span>{{ model.modelName || model.identifier }}</span>
<small>{{ model.identifier }}</small> <small>{{ model.identifier }}</small>
</div> </div>
@ -239,6 +289,7 @@
</main> </main>
</div> </div>
</div> </div>
<personal-center :info-visible.sync="infoVisible" :pwd-visible.sync="pwdVisible" />
</div> </div>
</template> </template>
@ -248,39 +299,64 @@ import vueSeamlessScroll from 'vue-seamless-scroll';
import drawMixin from '../../utils/bigScreen/drawMixin'; import drawMixin from '../../utils/bigScreen/drawMixin';
import { formatTime } from '../../utils/bigScreen/index.js'; import { formatTime } from '../../utils/bigScreen/index.js';
import { loadMap } from '@/utils/map'; import { loadMap } from '@/utils/map';
import { DEFAULT_TIANDITU_TILE_KEY } from '@/utils/tdtMap';
import chinaMapJson from '@/assets/echarts-map-json/china.json'; import chinaMapJson from '@/assets/echarts-map-json/china.json';
import { getToken, getUserId } from '@/utils/auth'; import { getToken, getUserId } from '@/utils/auth';
import { listAllDeviceShort, getDeviceStatistic, listThingsModel } from '@/api/iot/device'; import { getUserProfile } from '@/api/system/user';
import { listGroup, getDeviceIds } from '@/api/iot/group'; import { listDeviceShort, getDeviceShortStatistic, getDeviceStatistic, listThingsModel } from '@/api/iot/device';
import { listAlertLog } from '@/api/iot/alertLog'; import { listAlertLog } from '@/api/iot/alertLog';
import { listDeviceUser } from '@/api/iot/deviceuser';
import { getDataCenterDeviceHistory } from '@/api/data/center'; import { getDataCenterDeviceHistory } from '@/api/data/center';
import { listProject, getUserDeviceStats } from '@/api/iot/project'; import { listProject } from '@/api/iot/project';
import personalCenter from './personal-center.vue';
// 省份名称到文件名的映射
const provinceNameMap = {
'北京': 'beijing', '天津': 'tianjin', '河北': 'hebei', '山西': 'shanxi',
'内蒙古': 'neimenggu', '辽宁': 'liaoning', '吉林': 'jilin', '黑龙江': 'heilongjiang',
'上海': 'shanghai', '江苏': 'jiangsu', '浙江': 'zhejiang', '安徽': 'anhui',
'福建': 'fujian', '江西': 'jiangxi', '山东': 'shandong', '河南': 'henan',
'湖北': 'hubei', '湖南': 'hunan', '广东': 'guangdong', '广西': 'guangxi',
'海南': 'hainan', '重庆': 'chongqing', '四川': 'sichuan', '贵州': 'guizhou',
'云南': 'yunnan', '西藏': 'xizang', '陕西': 'shanxi1', '甘肃': 'gansu',
'青海': 'qinghai', '宁夏': 'ningxia', '新疆': 'xinjiang', '台湾': 'taiwan',
'香港': 'xianggang', '澳门': 'aomen'
};
const DEFAULT_CENTER = { lng: 102.72, lat: 25.04 }; const DEFAULT_CENTER = { lng: 102.72, lat: 25.04 };
// 人员设备视图:用户设备分页大小(非项目态按此页大小服务端分页,项目态按此翻页)
const USER_DEVICE_PAGE_SIZE = 10;
export default { export default {
name: 'BigScreenHome', name: 'BigScreenHome',
components: { vueSeamlessScroll }, components: { vueSeamlessScroll, personalCenter },
mixins: [drawMixin], mixins: [drawMixin],
data() { data() {
return { return {
activeMode: this.resolveModeByRoute(), activeMode: this.resolveModeByRoute(),
loading: true, loading: true,
analysisLoading: false, analysisLoading: false,
infoVisible: false,
pwdVisible: false,
headerUser: {},
dateDay: '', dateDay: '',
dateYear: '', dateYear: '',
dateWeek: '', dateWeek: '',
timing: null, timing: null,
weekday: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'], weekday: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
deviceStatistic: {}, deviceStatistic: {},
deviceList: [], projectDeviceList: [],
groupList: [],
realProjectList: [], realProjectList: [],
groupDeviceMap: {},
alarmList: [], alarmList: [],
userDeviceList: [], userDeviceList: [],
userDeviceStatsData: {}, userDeviceStatsData: {},
userDeviceTotal: 0,
userDevicePageNum: 1,
deviceLoadingMore: false,
projectDeviceVisible: 8,
analysisDeviceOptions: [],
selectedAnalysisDevice: null,
analysisDeviceSearchLoading: false,
selectedProject: null, selectedProject: null,
projectChart: null, projectChart: null,
deviceChart: null, deviceChart: null,
@ -291,6 +367,9 @@ export default {
satelliteLayer: null, satelliteLayer: null,
satelliteLabelLayer: null, satelliteLabelLayer: null,
satelliteMapLoading: false, satelliteMapLoading: false,
currentProvince: null,
provinceMapJson: null,
pendingProjectToFocus: null,
identifierList: [], identifierList: [],
analysisLineList: [], analysisLineList: [],
analysisQuery: { analysisQuery: {
@ -313,16 +392,31 @@ export default {
isScale() { isScale() {
return true; return true;
}, },
headerAvatar() {
return this.$store.getters.avatar || require('@/assets/images/profile.jpg');
},
headerUserName() {
return this.headerUser.nickName || this.headerUser.userName || this.$store.getters.name || '';
},
currentUserId() { currentUserId() {
return getUserId(); return getUserId();
}, },
onlineCount() { onlineCount() {
return this.deviceList.filter((item) => Number(item.status) === 3).length; return this.activeDeviceList.filter((item) => Number(item.status) === 3).length;
},
activeDeviceList() {
return this.selectedProject ? this.projectDeviceList : this.userDeviceList;
},
activeAlarmList() {
if (!this.selectedProject) return this.alarmList;
const serials = new Set(this.projectDeviceList.map(item => String(item.serialNumber)));
return this.alarmList.filter(item => serials.has(String(item.serialNumber)));
}, },
offlineCount() { offlineCount() {
return Math.max(this.deviceList.length - this.onlineCount, 0); return Math.max(this.activeDeviceList.length - this.onlineCount, 0);
}, },
alarmCount() { alarmCount() {
if (this.selectedProject) return this.activeAlarmList.length;
return Number(this.deviceStatistic.alertCount || this.alarmList.length || 0); return Number(this.deviceStatistic.alertCount || this.alarmList.length || 0);
}, },
warningProjectCount() { warningProjectCount() {
@ -339,16 +433,29 @@ export default {
runningProjectCount() { runningProjectCount() {
return Math.max(this.projectList.length - this.warningProjectCount - this.completedProjectCount, 0); return Math.max(this.projectList.length - this.warningProjectCount - this.completedProjectCount, 0);
}, },
userDeviceCount() { totalDeviceCount() {
if (Object.prototype.hasOwnProperty.call(this.userDeviceStatsData, 'totalDevices')) { if (this.selectedProject) return this.projectDeviceList.length;
return Number(this.userDeviceStatsData.totalDevices || 0); if (Object.prototype.hasOwnProperty.call(this.userDeviceStatsData, 'deviceCount')) {
return Number(this.userDeviceStatsData.deviceCount || 0);
} }
return this.userDeviceList.length || this.deviceList.length; return this.userDeviceTotal;
},
onlineDeviceCount() {
if (this.selectedProject) return this.onlineCount;
if (Object.prototype.hasOwnProperty.call(this.userDeviceStatsData, 'deviceOnlineCount')) {
return Number(this.userDeviceStatsData.deviceOnlineCount || 0);
}
return this.onlineCount;
},
offlineDeviceCount() {
if (this.selectedProject) return this.offlineCount;
if (Object.prototype.hasOwnProperty.call(this.userDeviceStatsData, 'deviceCount')) {
return Math.max(Number(this.userDeviceStatsData.deviceCount || 0) - this.onlineDeviceCount, 0);
}
return this.offlineCount;
}, },
mapSubtitle() { mapSubtitle() {
return this.realProjectList.length return this.selectedProject ? `${this.selectedProject.name}设备分布` : (this.realProjectList.length ? '项目设备数据来自 iot_device.project_id' : '当前未配置项目');
? '项目数据来自 iot_project 与项目分组关系'
: '当前未配置 iot_project,暂按设备分组聚合展示';
}, },
overviewMetrics() { overviewMetrics() {
return [ return [
@ -356,54 +463,47 @@ export default {
{ key: 'running', label: '运行项目', value: this.runningProjectCount }, { key: 'running', label: '运行项目', value: this.runningProjectCount },
{ key: 'warning', label: '预警项目', value: this.warningProjectCount }, { key: 'warning', label: '预警项目', value: this.warningProjectCount },
{ key: 'completed', label: '完结项目', value: this.completedProjectCount }, { key: 'completed', label: '完结项目', value: this.completedProjectCount },
{ key: 'device', label: '设备总数', value: this.deviceList.length || this.deviceStatistic.deviceCount || 0 }, { key: 'device', label: '设备总数', value: this.totalDeviceCount },
{ key: 'online', label: '在线设备', value: this.onlineCount || this.deviceStatistic.deviceOnlineCount || 0 }, { key: 'online', label: '在线设备', value: this.onlineDeviceCount },
{ key: 'offline', label: '离线设备', value: this.offlineCount }, { key: 'offline', label: '离线设备', value: this.offlineDeviceCount },
{ key: 'alarm', label: '告警数量', value: this.alarmCount }, { key: 'alarm', label: '告警数量', value: this.alarmCount },
]; ];
}, },
projectList() { projectList() {
if (this.realProjectList.length > 0) {
return this.realProjectList.map((project, index) => this.normalizeProject(project, index)); return this.realProjectList.map((project, index) => this.normalizeProject(project, index));
}
if (this.groupList.length) {
return this.groupList.map((group, index) => {
const devices = this.groupDeviceMap[group.groupId] || [];
const fallbackDevices = devices.length ? devices : this.deviceList.filter((_, deviceIndex) => deviceIndex % this.groupList.length === index);
return this.buildProjectFromDevices({
id: group.groupId,
name: group.groupName,
remark: group.remark,
devices: fallbackDevices,
index,
});
});
}
if (!this.deviceList.length) return [];
return [
this.buildProjectFromDevices({
id: 'default-project',
name: '当前用户项目',
remark: '',
devices: this.deviceList,
index: 0,
}),
];
}, },
projectPreview() { projectPreview() {
return this.projectList.slice(0, 8); return this.projectList.slice(0, 8);
}, },
userDevicePreview() { userDevicePreview() {
const directDevices = this.userDeviceList if (this.selectedProject) return this.projectDeviceList.slice(0, this.projectDeviceVisible);
.map((rel) => this.deviceList.find((device) => String(device.deviceId) === String(rel.deviceId))) return this.userDeviceList;
.filter(Boolean); },
return (directDevices.length ? directDevices : this.deviceList).slice(0, 8); userDeviceHasMore() {
return this.userDeviceList.length < this.userDeviceTotal;
},
showDeviceLoadMore() {
if (this.selectedProject) return this.projectDeviceVisible < this.projectDeviceList.length;
return this.userDeviceHasMore;
},
deviceMapPoints() {
return this.projectDeviceList
.filter(item => Number(item.longitude) && Number(item.latitude))
.map(item => ({
id: item.deviceId,
name: item.deviceName || item.serialNumber,
lng: Number(item.longitude),
lat: Number(item.latitude),
status: Number(item.status) === 3 ? 'running' : 'offline',
statusText: Number(item.status) === 3 ? '在线' : '离线',
device: item,
}));
}, },
mapPoints() { mapPoints() {
return this.projectList.filter((item) => Number(item.lng) && Number(item.lat)); return this.projectList.filter((item) => Number(item.lng) && Number(item.lat));
}, },
analysisTitle() { analysisTitle() {
const device = this.deviceList.find((item) => item.deviceId === this.analysisQuery.deviceId); const device = this.selectedAnalysisDevice;
if (!device) return '未选择设备'; if (!device) return '未选择设备';
return device.deviceName || device.serialNumber; return device.deviceName || device.serialNumber;
}, },
@ -422,7 +522,7 @@ export default {
}); });
}); });
}); });
return rows.slice(0, 200); return rows.slice(0, 100);
}, },
}, },
watch: { watch: {
@ -450,6 +550,7 @@ export default {
this.$nextTick(() => this.calcRate()); this.$nextTick(() => this.calcRate());
this.timeFn(); this.timeFn();
this.loadCockpitData(); this.loadCockpitData();
this.fetchHeaderUser();
}, },
beforeDestroy() { beforeDestroy() {
clearInterval(this.timing); clearInterval(this.timing);
@ -491,8 +592,31 @@ export default {
} }
}); });
}, },
goAdmin() { handleAdminCommand(command) {
if (command === 'admin') {
this.$router.push({ path: '/index' }).catch(() => {}); this.$router.push({ path: '/index' }).catch(() => {});
} else if (command === 'profile') {
this.infoVisible = true;
} else if (command === 'pwd') {
this.pwdVisible = true;
} else if (command === 'logout') {
this.$confirm('确定要退出当前账号吗?', '退出登录', {
confirmButtonText: '确定退出',
cancelButtonText: '取消',
type: 'warning',
customClass: 'tech-confirm-box',
center: true,
})
.then(() => {
const doLogout = getToken()
? this.$store.dispatch('LogOut').catch(() => this.$store.dispatch('FedLogOut'))
: this.$store.dispatch('FedLogOut');
doLogout.then(() => {
location.href = '/index';
});
})
.catch(() => {});
}
}, },
goHome() { goHome() {
if (this.$route.path !== '/bigscreen') { if (this.$route.path !== '/bigscreen') {
@ -515,6 +639,14 @@ export default {
setTime(); setTime();
this.timing = setInterval(setTime, 1000); this.timing = setInterval(setTime, 1000);
}, },
fetchHeaderUser() {
if (!getToken() && this.$route.query.share) return;
getUserProfile()
.then((res) => {
this.headerUser = res.data || {};
})
.catch(() => {});
},
async loadCockpitData() { async loadCockpitData() {
this.loading = true; this.loading = true;
if (!getToken() && this.$route.query.share) { if (!getToken() && this.$route.query.share) {
@ -528,14 +660,11 @@ export default {
try { try {
await Promise.all([ await Promise.all([
this.fetchDeviceStatistic(), this.fetchDeviceStatistic(),
this.fetchDevices(),
this.fetchGroups(),
this.fetchAlarms(), this.fetchAlarms(),
this.fetchUserDevices(), this.fetchUserDevices(),
this.fetchProjects(), this.fetchProjects(),
this.fetchUserDeviceStats() this.fetchUserDeviceStats()
]); ]);
await this.fetchGroupDevices();
this.setDefaultAnalysisDevice(); this.setDefaultAnalysisDevice();
} finally { } finally {
this.loading = false; this.loading = false;
@ -552,24 +681,6 @@ export default {
}) })
.catch(() => {}); .catch(() => {});
}, },
fetchDevices() {
return listAllDeviceShort({ pageNum: 1, pageSize: 9999 })
.then((res) => {
this.deviceList = res.rows || [];
})
.catch(() => {
this.deviceList = [];
});
},
fetchGroups() {
return listGroup({ pageNum: 1, pageSize: 9999 })
.then((res) => {
this.groupList = res.rows || [];
})
.catch(() => {
this.groupList = [];
});
},
fetchAlarms() { fetchAlarms() {
return listAlertLog({ pageNum: 1, pageSize: 20 }) return listAlertLog({ pageNum: 1, pageSize: 20 })
.then((res) => { .then((res) => {
@ -580,15 +691,41 @@ export default {
}); });
}, },
fetchUserDevices() { fetchUserDevices() {
if (!this.currentUserId) return Promise.resolve(); this.userDevicePageNum = 1;
return listDeviceUser({ userId: this.currentUserId, pageNum: 1, pageSize: 9999 }) this.userDeviceTotal = 0;
return listDeviceShort({ pageNum: 1, pageSize: USER_DEVICE_PAGE_SIZE, showChild: true, entrance: 2 })
.then((res) => { .then((res) => {
this.userDeviceList = res.rows || []; this.userDeviceList = res.rows || [];
this.userDeviceTotal = Number(res.total || 0);
}) })
.catch(() => { .catch(() => {
this.userDeviceList = []; this.userDeviceList = [];
this.userDeviceTotal = 0;
}); });
}, },
loadMoreUserDevices() {
if (this.deviceLoadingMore || !this.userDeviceHasMore) return;
this.deviceLoadingMore = true;
listDeviceShort({ pageNum: this.userDevicePageNum + 1, pageSize: USER_DEVICE_PAGE_SIZE, showChild: true, entrance: 2 })
.then((res) => {
this.userDeviceList = this.userDeviceList.concat(res.rows || []);
this.userDeviceTotal = Number(res.total || this.userDeviceTotal);
this.userDevicePageNum += 1;
})
.catch(() => {})
.finally(() => {
this.deviceLoadingMore = false;
});
},
loadMoreProjectDevices() {
this.projectDeviceVisible += USER_DEVICE_PAGE_SIZE;
},
fetchProjectDevices(projectId) {
this.projectDeviceVisible = 8;
return listDeviceShort({ pageNum: 1, pageSize: 9999, showChild: true, entrance: 2, projectId })
.then((res) => { this.projectDeviceList = res.rows || []; })
.catch(() => { this.projectDeviceList = []; });
},
fetchProjects() { fetchProjects() {
const query = { pageNum: 1, pageSize: 9999 }; const query = { pageNum: 1, pageSize: 9999 };
if (this.currentUserId) query.responsibleUserId = this.currentUserId; if (this.currentUserId) query.responsibleUserId = this.currentUserId;
@ -601,8 +738,7 @@ export default {
}); });
}, },
fetchUserDeviceStats() { fetchUserDeviceStats() {
if (!this.currentUserId) return Promise.resolve(); return getDeviceShortStatistic({ showChild: true, entrance: 2 })
return getUserDeviceStats(this.currentUserId)
.then((res) => { .then((res) => {
if (res.code === 200) { if (res.code === 200) {
this.userDeviceStatsData = res.data || {}; this.userDeviceStatsData = res.data || {};
@ -629,48 +765,38 @@ export default {
responsibleUserName: project.responsibleUserName, responsibleUserName: project.responsibleUserName,
}; };
}, },
async fetchGroupDevices() {
const groups = this.groupList.slice(0, 30);
const pairs = await Promise.all(
groups.map((group) =>
getDeviceIds(group.groupId)
.then((res) => [group.groupId, res.data || []])
.catch(() => [group.groupId, []])
)
);
const map = {};
pairs.forEach(([groupId, ids]) => {
const idSet = new Set((ids || []).map((id) => String(id)));
map[groupId] = this.deviceList.filter((device) => idSet.has(String(device.deviceId)));
});
this.groupDeviceMap = map;
},
buildProjectFromDevices({ id, name, remark, devices, index }) {
const validDevices = devices || [];
const points = validDevices.filter((device) => Number(device.longitude) && Number(device.latitude));
const lng = points.length ? points.reduce((sum, device) => sum + Number(device.longitude), 0) / points.length : DEFAULT_CENTER.lng + index * 0.08;
const lat = points.length ? points.reduce((sum, device) => sum + Number(device.latitude), 0) / points.length : DEFAULT_CENTER.lat + index * 0.05;
const onlineCount = validDevices.filter((device) => Number(device.status) === 3).length;
const isCompleted = /完结|已完成|结束|completed/i.test(String(remark || ''));
const hasWarning = !isCompleted && this.alarmCount > 0 && index === 0;
const status = isCompleted ? 'completed' : hasWarning ? 'warning' : 'running';
return {
id,
name: name || '未命名项目',
devices: validDevices,
deviceCount: validDevices.length,
onlineCount,
offlineCount: Math.max(validDevices.length - onlineCount, 0),
lng,
lat,
status,
statusText: status === 'warning' ? '预警' : status === 'completed' ? '完结' : '运行',
};
},
setDefaultAnalysisDevice() { setDefaultAnalysisDevice() {
if (!this.analysisQuery.deviceId && this.deviceList.length) { if (this.analysisQuery.deviceId) return;
this.analysisQuery.deviceId = this.deviceList[0].deviceId; const devices = this.selectedProject ? this.projectDeviceList : this.userDeviceList;
this.handleAnalysisDeviceChange(this.analysisQuery.deviceId); if (!devices.length) return;
const device = devices[0];
this.selectedAnalysisDevice = device;
this.analysisQuery.deviceId = device.deviceId;
this.searchAnalysisDevices('').then(() => {
this.handleAnalysisDeviceChange(device.deviceId);
});
},
searchAnalysisDevices(keyword) {
const kw = String(keyword || '').trim();
const query = { pageNum: 1, pageSize: 10, showChild: true, entrance: 2 };
// 选中项目后,分析设备只从当前项目范围内检索
if (this.selectedProject) query.projectId = this.selectedProject.id;
if (kw) query.deviceName = kw;
this.analysisDeviceSearchLoading = true;
return listDeviceShort(query)
.then((res) => {
this.analysisDeviceOptions = res.rows || [];
})
.catch(() => {
this.analysisDeviceOptions = [];
})
.finally(() => {
this.analysisDeviceSearchLoading = false;
});
},
onAnalysisDeviceVisible(visible) {
if (visible && this.analysisDeviceOptions.length === 0) {
this.searchAnalysisDevices('');
} }
}, },
renderCharts() { renderCharts() {
@ -722,8 +848,8 @@ export default {
type: 'bar', type: 'bar',
barWidth: 28, barWidth: 28,
data: [ data: [
{ value: this.onlineCount, itemStyle: { color: '#18f2a3' } }, { value: this.onlineDeviceCount, itemStyle: { color: '#18f2a3' } },
{ value: this.offlineCount, itemStyle: { color: '#4098ff' } }, { value: this.offlineDeviceCount, itemStyle: { color: '#4098ff' } },
{ value: this.alarmCount, itemStyle: { color: '#ff5574' } }, { value: this.alarmCount, itemStyle: { color: '#ff5574' } },
], ],
}, },
@ -744,9 +870,16 @@ export default {
this.$echarts.registerMap('china', chinaMapJson); this.$echarts.registerMap('china', chinaMapJson);
this[chartKey] = this.$echarts.init(mapRef); this[chartKey] = this.$echarts.init(mapRef);
this[chartKey].on('click', (params) => { this[chartKey].on('click', (params) => {
if (params.seriesType !== 'effectScatter') return; // 点击散点(项目)
if (params.seriesType === 'effectScatter') {
const project = this.projectList.find((item) => String(item.id) === String(params.data.id)); const project = this.projectList.find((item) => String(item.id) === String(params.data.id));
if (project) this.selectProject(project); if (project) this.selectProject(project);
return;
}
// 点击省份区域
if (params.componentType === 'geo' && params.name) {
this.loadProvinceMap(params.name);
}
}); });
} }
const points = this.mapPoints.map((project) => ({ const points = this.mapPoints.map((project) => ({
@ -757,7 +890,12 @@ export default {
statusText: project.statusText, statusText: project.statusText,
onlineCount: project.onlineCount, onlineCount: project.onlineCount,
})); }));
if (!this.selectedProject && this.mapPoints.length) this.selectedProject = this.mapPoints[0];
// 如果在省份视图,过滤该省份的设备
const displayPoints = this.currentProvince
? points.filter(point => this.isPointInProvince(point.value[0], point.value[1]))
: points;
this[chartKey].setOption( this[chartKey].setOption(
{ {
backgroundColor: 'transparent', backgroundColor: 'transparent',
@ -771,10 +909,10 @@ export default {
}, },
}, },
geo: { geo: {
map: 'china', map: this.currentProvince || 'china',
roam: false, roam: false,
layoutCenter: layoutConfig.layoutCenter, layoutCenter: layoutConfig.layoutCenter,
layoutSize: layoutConfig.layoutSize, layoutSize: this.currentProvince ? '90%' : layoutConfig.layoutSize,
aspectScale: 0.82, aspectScale: 0.82,
label: { label: {
show: true, show: true,
@ -800,7 +938,7 @@ export default {
name: '项目分布', name: '项目分布',
type: 'effectScatter', type: 'effectScatter',
coordinateSystem: 'geo', coordinateSystem: 'geo',
data: points, data: displayPoints,
symbolSize: (value) => Math.max(13, Math.min(30, 12 + Number(value[2] || 0) * 2)), symbolSize: (value) => Math.max(13, Math.min(30, 12 + Number(value[2] || 0) * 2)),
rippleEffect: { brushType: 'stroke', scale: 4.5 }, rippleEffect: { brushType: 'stroke', scale: 4.5 },
label: { label: {
@ -844,20 +982,32 @@ export default {
attributionControl: false, attributionControl: false,
worldCopyJump: true, worldCopyJump: true,
}); });
this.satelliteLayer = L.tileLayer('https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', { // 天地图 WMTS(Web 墨卡托,TILEMATRIXSET=w),与 Leaflet 默认坐标系一致
subdomains: ['1', '2', '3', '4'], const tdtTile = (layer) => L.tileLayer(
`https://t{s}.tianditu.gov.cn/${layer}_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${layer}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${DEFAULT_TIANDITU_TILE_KEY}`,
{
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
maxZoom: 18, maxZoom: 18,
}).addTo(this.satelliteMap); }
this.satelliteLabelLayer = L.tileLayer('https://webst0{s}.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}', { );
subdomains: ['1', '2', '3', '4'], this.satelliteLayer = tdtTile('img').addTo(this.satelliteMap);
maxZoom: 18, this.satelliteLabelLayer = tdtTile('cia').addTo(this.satelliteMap);
}).addTo(this.satelliteMap);
} }
this.renderSatelliteMarkers(L); this.renderSatelliteMarkers(L);
this.$nextTick(() => { this.$nextTick(() => {
if (!this.satelliteMap) return; if (!this.satelliteMap) return;
this.satelliteMap.invalidateSize(); this.satelliteMap.invalidateSize();
// 如果有待定位的项目,定位到该项目;否则适应所有项目
if (this.pendingProjectToFocus) {
const project = this.pendingProjectToFocus;
this.pendingProjectToFocus = null;
this.satelliteMap.setView([Number(project.lat), Number(project.lng)], 10, {
animate: true,
duration: 0.8
});
} else {
this.fitSatelliteMap(); this.fitSatelliteMap();
}
}); });
}) })
.catch(() => { .catch(() => {
@ -868,37 +1018,40 @@ export default {
}); });
}, },
getSatelliteCenter() { getSatelliteCenter() {
if (!this.mapPoints.length) return DEFAULT_CENTER; const points = this.selectedProject ? this.deviceMapPoints : this.mapPoints;
const lng = this.mapPoints.reduce((sum, point) => sum + Number(point.lng), 0) / this.mapPoints.length; if (!points.length) return DEFAULT_CENTER;
const lat = this.mapPoints.reduce((sum, point) => sum + Number(point.lat), 0) / this.mapPoints.length; const lng = points.reduce((sum, point) => sum + Number(point.lng), 0) / points.length;
const lat = points.reduce((sum, point) => sum + Number(point.lat), 0) / points.length;
return { lng, lat }; return { lng, lat };
}, },
renderSatelliteMarkers(L) { renderSatelliteMarkers(L) {
if (!this.satelliteMap) return; if (!this.satelliteMap) return;
this.satelliteMarkers.forEach((marker) => marker.remove()); this.satelliteMarkers.forEach((marker) => marker.remove());
this.satelliteMarkers = this.mapPoints.map((project) => { const points = this.selectedProject ? this.deviceMapPoints : this.mapPoints;
const marker = L.marker([Number(project.lat), Number(project.lng)], { this.satelliteMarkers = points.map((point) => {
const marker = L.marker([Number(point.lat), Number(point.lng)], {
icon: L.divIcon({ icon: L.divIcon({
className: '', className: '',
html: `<div class="satellite-marker satellite-marker--${project.status}"><span></span><b>${project.name || ''}</b></div>`, html: `<div class="satellite-marker satellite-marker--${point.status}"><span></span><b>${point.name || ''}</b></div>`,
iconSize: [180, 34], iconSize: [180, 34],
iconAnchor: [12, 17], iconAnchor: [12, 17],
}), }),
}).addTo(this.satelliteMap); }).addTo(this.satelliteMap);
marker.on('click', () => this.selectProject(project)); if (!this.selectedProject) marker.on('click', () => this.selectProject(point));
return marker; return marker;
}); });
}, },
fitSatelliteMap() { fitSatelliteMap() {
if (!this.satelliteMap) return; if (!this.satelliteMap) return;
if (!this.mapPoints.length) { const points = this.selectedProject ? this.deviceMapPoints : this.mapPoints;
if (!points.length) {
this.satelliteMap.setView([DEFAULT_CENTER.lat, DEFAULT_CENTER.lng], 4); this.satelliteMap.setView([DEFAULT_CENTER.lat, DEFAULT_CENTER.lng], 4);
return; return;
} }
const bounds = this.mapPoints.map((point) => [Number(point.lat), Number(point.lng)]); const bounds = points.map((point) => [Number(point.lat), Number(point.lng)]);
this.satelliteMap.fitBounds(bounds, { this.satelliteMap.fitBounds(bounds, {
padding: [120, 120], padding: [120, 120],
maxZoom: this.mapPoints.length === 1 ? 16 : 11, maxZoom: points.length === 1 ? 16 : 11,
}); });
}, },
zoomSatelliteMap(delta) { zoomSatelliteMap(delta) {
@ -907,24 +1060,66 @@ export default {
}, },
selectProject(project) { selectProject(project) {
this.selectedProject = project; this.selectedProject = project;
this.fetchProjectDevices(project.id).then(() => {
this.refreshAnalysisDeviceScope();
this.switchMode('mapView');
this.$nextTick(() => this.renderSatelliteMap());
});
}, },
focusDevice(device) { isProjectSelected(project) {
const project = this.projectList.find((item) => (item.devices || []).some((dev) => dev.deviceId === device.deviceId)); return !!(this.selectedProject && String(this.selectedProject.id) === String(project.id));
if (project) { },
this.selectProject(project); handleProjectRowClick(project) {
// 再次点击当前选中的项目:取消选中并返回总揽模式
if (this.isProjectSelected(project)) {
this.selectedProject = null;
this.refreshAnalysisDeviceScope();
if (this.$route.path !== '/bigscreen') {
this.$router.push({ path: '/bigscreen' }).catch(() => {});
}
this.switchMode('screen');
return; return;
} }
if (!Number(device.longitude) || !Number(device.latitude) || !this.mapPoints.length) return; this.selectProject(project);
const nearestProject = this.mapPoints.reduce((best, current) => { },
const bestDistance = best refreshAnalysisDeviceScope() {
? Math.abs(Number(device.longitude) - Number(best.lng)) + Math.abs(Number(device.latitude) - Number(best.lat)) // 已选分析设备不在当前项目范围内时,清空选择与图表数据
: Number.POSITIVE_INFINITY; const selected = this.selectedAnalysisDevice;
const currentDistance = Math.abs(Number(device.longitude) - Number(current.lng)) + Math.abs(Number(device.latitude) - Number(current.lat)); const inScope = !selected ||
return currentDistance < bestDistance ? current : best; !this.selectedProject ||
}, null); this.projectDeviceList.some((item) => String(item.deviceId) === String(selected.deviceId));
if (nearestProject) this.selectProject(nearestProject); if (!inScope) {
this.selectedAnalysisDevice = null;
this.analysisQuery.deviceId = null;
this.identifierList = [];
this.analysisLineList = [];
}
// 刷新下拉选项,使其跟随当前项目范围
this.searchAnalysisDevices('');
},
focusDeviceAnalysis(device) {
// 点击设备直接进入设备分析页面,不再跳转地图
this.selectedAnalysisDevice = device;
this.analysisQuery.deviceId = device.deviceId;
// 保证分析页设备下拉框能正确回显设备名称
const exists = this.analysisDeviceOptions.some((item) => String(item.deviceId) === String(device.deviceId));
if (!exists) this.analysisDeviceOptions = [device].concat(this.analysisDeviceOptions);
this.identifierList = [];
this.analysisLineList = [];
this.switchMode('analysis');
listThingsModel({ deviceId: device.deviceId, pageNum: 1, pageSize: 9999 })
.then((res) => {
if (res.code === 200) this.identifierList = res.rows || [];
})
.catch(() => {
this.identifierList = [];
});
this.runAnalysis();
}, },
handleAnalysisDeviceChange(deviceId) { handleAnalysisDeviceChange(deviceId) {
this.selectedAnalysisDevice = deviceId
? this.analysisDeviceOptions.find((item) => String(item.deviceId) === String(deviceId)) || null
: null;
this.identifierList = []; this.identifierList = [];
this.analysisLineList = []; this.analysisLineList = [];
if (!deviceId) return; if (!deviceId) return;
@ -937,7 +1132,7 @@ export default {
}); });
}, },
runAnalysis() { runAnalysis() {
const device = this.deviceList.find((item) => item.deviceId === this.analysisQuery.deviceId); const device = this.selectedAnalysisDevice;
if (!device) { if (!device) {
this.$message.warning('请先选择设备'); this.$message.warning('请先选择设备');
return; return;
@ -1030,6 +1225,61 @@ export default {
this.satelliteMap = null; this.satelliteMap = null;
} }
}, },
async loadProvinceMap(provinceName) {
const fileName = provinceNameMap[provinceName];
if (!fileName) {
//this.$message.warning(`暂无 ${provinceName} 的地图数据`);
return;
}
try {
// 使用 require 动态加载,Webpack 可以识别
const provinceJson = require(`@/assets/echarts-map-json/province/${fileName}.json`);
this.provinceMapJson = provinceJson;
this.currentProvince = provinceName;
this.$echarts.registerMap(provinceName, this.provinceMapJson);
this.renderDashboardMap();
} catch (error) {
console.error('加载省份地图失败:', error);
this.$message.error(`加载 ${provinceName} 地图失败`);
}
},
backToChina() {
this.currentProvince = null;
this.provinceMapJson = null;
this.renderDashboardMap();
},
isPointInProvince(lng, lat) {
if (!this.currentProvince || !this.provinceMapJson) return true;
// 简单的边界框检测(可以根据需要优化为更精确的多边形判断)
try {
const features = this.provinceMapJson.features || [];
if (!features.length) return false;
// 计算省份的边界框
let minLng = Infinity, maxLng = -Infinity;
let minLat = Infinity, maxLat = -Infinity;
features.forEach(feature => {
if (feature.properties && feature.properties.cp) {
const [cpLng, cpLat] = feature.properties.cp;
minLng = Math.min(minLng, cpLng);
maxLng = Math.max(maxLng, cpLng);
minLat = Math.min(minLat, cpLat);
maxLat = Math.max(maxLat, cpLat);
}
});
// 扩大边界10%以包含边缘设备
const lngMargin = (maxLng - minLng) * 0.1;
const latMargin = (maxLat - minLat) * 0.1;
return lng >= (minLng - lngMargin) && lng <= (maxLng + lngMargin) &&
lat >= (minLat - latMargin) && lat <= (maxLat + latMargin);
} catch (error) {
console.error('边界检测失败:', error);
return true;
}
},
}, },
}; };
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@ -451,6 +451,7 @@ export default {
deviceName: null, deviceName: null,
productId: null, productId: null,
groupId: null, groupId: null,
projectId: null,
deptId: null, deptId: null,
productName: null, productName: null,
userId: null, userId: null,
@ -494,6 +495,15 @@ export default {
this.queryParams.groupId = null; this.queryParams.groupId = null;
this.queryParams.serialNumber = null; this.queryParams.serialNumber = null;
} }
// 项目筛选
let projectId = this.$route.query.projectId;
if (projectId != null) {
this.queryParams.projectId = Number(projectId);
this.queryParams.groupId = null;
this.queryParams.productId = null;
this.queryParams.serialNumber = null;
this.searchShow = true;
}
// 分组筛选 // 分组筛选
let groupId = this.$route.query.groupId; let groupId = this.$route.query.groupId;
if (groupId != null) { if (groupId != null) {
@ -534,6 +544,15 @@ export default {
this.queryParams.groupId = null; this.queryParams.groupId = null;
this.queryParams.serialNumber = null; this.queryParams.serialNumber = null;
} }
// 项目筛选
let projectId = this.$route.query.projectId;
if (projectId != null) {
this.queryParams.projectId = Number(projectId);
this.queryParams.groupId = null;
this.queryParams.productId = null;
this.queryParams.serialNumber = null;
this.searchShow = true;
}
// 分组筛选 // 分组筛选
let groupId = this.$route.query.groupId; let groupId = this.$route.query.groupId;
if (groupId != null) { if (groupId != null) {

View File

@ -0,0 +1,138 @@
<template>
<el-dialog :title="`管理设备 - ${projectName}`" :visible.sync="open" width="82%" append-to-body :close-on-click-modal="false" @closed="resetState">
<div class="device-select-summary">
<div><span>当前项目已选</span><strong>{{ selectedDeviceIds.length }}</strong><span>台设备</span></div>
<el-alert v-if="transferCount" :title="`其中 ${transferCount} 台设备当前属于其他项目,保存后将转移到本项目`" type="warning" :closable="false" show-icon />
</div>
<el-card shadow="never" class="group-quick-select">
<el-form inline size="small">
<el-form-item label="按设备分组快速添加">
<el-select v-model="selectedGroupIds" multiple collapse-tags filterable placeholder="请选择设备分组" style="width: 360px">
<el-option v-for="group in groupList" :key="group.groupId" :label="group.groupName" :value="group.groupId" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" plain icon="el-icon-plus" @click="addGroupDevices">添加分组内设备</el-button></el-form-item>
<span class="quick-tip">分组仅用于快速勾选,项目归属以设备为准</span>
</el-form>
</el-card>
<el-form ref="queryForm" :model="queryParams" inline size="small" class="device-query">
<el-form-item label="设备名称" prop="deviceName"><el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="设备编号" prop="serialNumber"><el-input v-model="queryParams.serialNumber" placeholder="请输入设备编号" clearable @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="设备状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 130px">
<el-option label="未激活" :value="1" /><el-option label="禁用" :value="2" /><el-option label="在线" :value="3" /><el-option label="离线" :value="4" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button><el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<el-table ref="deviceTable" v-loading="loading" :data="deviceList" :row-key="row => String(row.deviceId)" height="430" border @select="handleRowSelect" @select-all="handleSelectAll">
<el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
<el-table-column label="设备名称" prop="deviceName" min-width="150" show-overflow-tooltip />
<el-table-column label="设备编号" prop="serialNumber" min-width="160" show-overflow-tooltip />
<el-table-column label="产品名称" prop="productName" min-width="130" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center"><template slot-scope="scope"><el-tag :type="Number(scope.row.status) === 3 ? 'success' : 'info'" size="small">{{ getStatusText(scope.row.status) }}</el-tag></template></el-table-column>
<el-table-column label="当前项目归属" min-width="150" align="center">
<template slot-scope="scope">
<el-tag v-if="isCurrentProject(scope.row)" type="success" size="small">当前项目</el-tag>
<el-tag v-else-if="scope.row.projectId" type="warning" size="small">{{ getProjectName(scope.row.projectId) }}</el-tag>
<span v-else class="unbound-text">未绑定</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<div slot="footer" class="dialog-footer"><el-button @click="open = false">取 消</el-button><el-button type="primary" :loading="saving" @click="handleConfirm">保 存</el-button></div>
</el-dialog>
</template>
<script>
import { listGroup, getDeviceIds } from '@/api/iot/group'
import { listDeviceShort } from '@/api/iot/device'
import { listProject, getProjectDeviceIds, updateProjectDevices } from '@/api/iot/project'
export default {
name: 'ProjectDeviceSelect',
data() {
return { open: false, loading: false, saving: false, projectId: null, projectName: '', deviceList: [], total: 0, groupList: [], projectList: [], selectedGroupIds: [], selectedDeviceIds: [], deviceProjectMap: {}, queryParams: this.createQueryParams() }
},
computed: {
transferCount() {
return this.selectedDeviceIds.filter(id => {
const sourceProjectId = this.deviceProjectMap[String(id)]
return sourceProjectId && String(sourceProjectId) !== String(this.projectId)
}).length
}
},
methods: {
createQueryParams() {
return {
pageNum: 1,
pageSize: 10,
entrance: 2,
projectId: null,
projectDeviceMode: true,
showChild: true,
deviceName: null,
serialNumber: null,
status: null
}
},
async show(projectId, projectName) {
this.resetState(); this.projectId = projectId; this.projectName = projectName || ''; this.queryParams.projectId = projectId; this.open = true; this.loading = true
try {
const [deviceIdsResponse, groupResponse, projectResponse] = await Promise.all([getProjectDeviceIds(projectId), listGroup({ pageNum: 1, pageSize: 1000 }), listProject({ pageNum: 1, pageSize: 9999 })])
this.selectedDeviceIds = (deviceIdsResponse.data || []).map(id => String(id)); this.groupList = groupResponse.rows || []; this.projectList = projectResponse.rows || []
await this.getList()
} finally { this.loading = false }
},
async getList() {
this.loading = true
try {
const response = await listDeviceShort(this.queryParams); this.deviceList = response.rows || []; this.total = response.total || 0
this.deviceList.forEach(device => this.$set(this.deviceProjectMap, String(device.deviceId), device.projectId || null)); this.restoreSelection()
} finally { this.loading = false }
},
restoreSelection() {
this.$nextTick(() => { if (this.$refs.deviceTable) this.deviceList.forEach(device => this.$refs.deviceTable.toggleRowSelection(device, this.selectedDeviceIds.includes(String(device.deviceId)))) })
},
handleRowSelect(selection, row) {
const id = String(row.deviceId); const checked = selection.some(item => String(item.deviceId) === id)
if (checked && !this.selectedDeviceIds.includes(id)) this.selectedDeviceIds.push(id)
if (!checked) this.selectedDeviceIds = this.selectedDeviceIds.filter(item => item !== id)
},
handleSelectAll(selection) {
const selectedIds = new Set(selection.map(item => String(item.deviceId)))
this.deviceList.forEach(device => { const id = String(device.deviceId); if (selectedIds.has(id) && !this.selectedDeviceIds.includes(id)) this.selectedDeviceIds.push(id); if (!selectedIds.has(id)) this.selectedDeviceIds = this.selectedDeviceIds.filter(item => item !== id) })
},
async addGroupDevices() {
if (!this.selectedGroupIds.length) { this.$modal.msgWarning('请先选择设备分组'); return }
const responses = await Promise.all(this.selectedGroupIds.map(groupId => getDeviceIds(groupId))); const ids = responses.reduce((result, response) => result.concat(response.data || []), [])
this.selectedDeviceIds = Array.from(new Set(this.selectedDeviceIds.concat(ids.map(id => String(id))))); this.restoreSelection(); this.$modal.msgSuccess(`已快速添加 ${new Set(ids.map(id => String(id))).size} 台设备`)
},
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.queryParams = this.createQueryParams(); this.getList() },
async handleConfirm() {
if (this.transferCount) await this.$modal.confirm(`有 ${this.transferCount} 台设备将从其他项目转移到“${this.projectName}”,是否继续?`)
this.saving = true
try { await updateProjectDevices(this.projectId, this.selectedDeviceIds); this.$modal.msgSuccess('项目设备保存成功'); this.open = false; this.$emit('refresh') } finally { this.saving = false }
},
getProjectName(projectId) { const project = this.projectList.find(item => String(item.projectId) === String(projectId)); return project ? project.projectName : `项目 ${projectId}` },
isCurrentProject(device) { return device.projectId && String(device.projectId) === String(this.projectId) },
getStatusText(status) { return { 1: '未激活', 2: '禁用', 3: '在线', 4: '离线' }[Number(status)] || '未知' },
resetState() { this.loading = false; this.saving = false; this.projectId = null; this.projectName = ''; this.deviceList = []; this.total = 0; this.groupList = []; this.projectList = []; this.selectedGroupIds = []; this.selectedDeviceIds = []; this.deviceProjectMap = {}; this.queryParams = this.createQueryParams() }
}
}
</script>
<style scoped>
.device-select-summary { display: flex; align-items: center; gap: 24px; margin-bottom: 14px; }
.device-select-summary strong { margin: 0 6px; color: #409eff; font-size: 22px; }
.device-select-summary .el-alert { flex: 1; }
.group-quick-select { margin-bottom: 16px; background: #f8fafc; }
.group-quick-select .el-form-item { margin-bottom: 0; }
.quick-tip, .unbound-text { color: #909399; font-size: 12px; }
.device-query { margin-bottom: 4px; }
</style>

View File

@ -62,9 +62,10 @@
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="260"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="250">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="small" type="text" icon="el-icon-view" @click="handleView(scope.row)" v-hasPermi="['iot:project:query']">查看</el-button> <el-button size="small" type="text" icon="el-icon-view" @click="handleView(scope.row)" v-hasPermi="['iot:project:query']">查看</el-button>
<el-button size="small" type="text" icon="el-icon-setting" @click="handleManageDevices(scope.row)" v-hasPermi="['iot:project:edit']">管理设备</el-button>
<el-button size="small" type="text" icon="el-icon-monitor" @click="handleViewDevices(scope.row)" v-hasPermi="['iot:project:query']">查看设备</el-button> <el-button size="small" type="text" icon="el-icon-monitor" @click="handleViewDevices(scope.row)" v-hasPermi="['iot:project:query']">查看设备</el-button>
<el-button size="small" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['iot:project:edit']">编辑</el-button> <el-button size="small" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['iot:project:edit']">编辑</el-button>
<el-button size="small" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['iot:project:remove']">删除</el-button> <el-button size="small" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['iot:project:remove']">删除</el-button>
@ -120,11 +121,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-form-item label="项目地址" prop="address">
<el-input v-model="form.address" placeholder="请输入项目地址" :disabled="isView" />
</el-form-item>
<el-form-item label="项目坐标" prop="coordinates"> <el-form-item label="项目坐标" prop="coordinates">
<el-row :gutter="10"> <el-row :gutter="10" type="flex" justify="start" style="flex-wrap: nowrap;">
<el-col :span="8"> <el-col :span="8">
<el-input v-model.number="form.longitude" placeholder="经度" type="number" :disabled="isView" /> <el-input v-model.number="form.longitude" placeholder="经度" type="number" :disabled="isView" />
</el-col> </el-col>
@ -136,10 +134,8 @@
</el-col> </el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>
<el-form-item label="关联分组" prop="groupIds"> <el-form-item label="项目地址" prop="address">
<el-select v-model="form.groupIds" multiple placeholder="请选择设备分组" style="width: 100%" :disabled="isView"> <el-input v-model="form.address" placeholder="请输入项目地址" :disabled="isView" />
<el-option v-for="group in groupList" :key="group.groupId" :label="group.groupName" :value="group.groupId" />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="项目描述" prop="projectDesc"> <el-form-item label="项目描述" prop="projectDesc">
<el-input v-model="form.projectDesc" type="textarea" placeholder="请输入项目描述" :rows="3" :disabled="isView" /> <el-input v-model="form.projectDesc" type="textarea" placeholder="请输入项目描述" :rows="3" :disabled="isView" />
@ -174,31 +170,21 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 多分组查看设备选择 --> <project-device-select ref="deviceSelect" @refresh="getList" />
<el-dialog title="选择设备分组" :visible.sync="groupSelectOpen" width="420px" append-to-body>
<el-form label-width="90px" size="mini">
<el-form-item label="设备分组">
<el-select v-model="selectedGroupId" placeholder="请选择设备分组" style="width: 100%">
<el-option v-for="group in selectableGroupList" :key="group.groupId" :label="group.groupName" :value="group.groupId" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmViewDevices">确 定</el-button>
<el-button @click="groupSelectOpen = false">取 消</el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { listProject, getProject, delProject, addProject, updateProject } from '@/api/iot/project'; import { listProject, getProject, delProject, addProject, updateProject } from '@/api/iot/project';
import { listGroup } from '@/api/iot/group';
import { listUser } from '@/api/system/user'; import { listUser } from '@/api/system/user';
import { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY, loadTiandituMap } from '@/utils/tdtMap'; import { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY, loadTiandituMap } from '@/utils/tdtMap';
import ProjectDeviceSelect from './device-select.vue';
export default { export default {
name: 'Project', name: 'Project',
components: {
ProjectDeviceSelect
},
data() { data() {
return { return {
loading: true, loading: true,
@ -209,18 +195,14 @@ export default {
total: 0, total: 0,
projectList: [], projectList: [],
userList: [], userList: [],
groupList: [],
title: '', title: '',
open: false, open: false,
isView: false, isView: false,
mapPickerOpen: false, mapPickerOpen: false,
groupSelectOpen: false,
pickerLongitude: null, pickerLongitude: null,
pickerLatitude: null, pickerLatitude: null,
pickerAddress: '', pickerAddress: '',
geocoding: false, geocoding: false,
selectableGroupList: [],
selectedGroupId: null,
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
@ -243,7 +225,6 @@ export default {
created() { created() {
this.getList(); this.getList();
this.getUserList(); this.getUserList();
this.getGroupList();
}, },
beforeDestroy() { beforeDestroy() {
this.disposeMap(); this.disposeMap();
@ -266,18 +247,16 @@ export default {
this.userList = response.rows || []; this.userList = response.rows || [];
}); });
}, },
/** 查询分组列表 */
getGroupList() {
listGroup({ pageNum: 1, pageSize: 1000 }).then(response => {
this.groupList = response.rows || [];
});
},
/** 获取负责人名称 */ /** 获取负责人名称 */
getUserName(userId, userName) { getUserName(userId, userName) {
if (userName) return userName; if (userName) return userName;
const user = this.userList.find(item => Number(item.userId) === Number(userId)); const user = this.userList.find(item => Number(item.userId) === Number(userId));
return user ? user.nickName : ''; return user ? user.nickName : '';
}, },
/** 管理项目设备 */
handleManageDevices(row) {
this.$refs.deviceSelect.show(row.projectId, row.projectName);
},
/** 负责人变更 */ /** 负责人变更 */
handleResponsibleUserChange(userId) { handleResponsibleUserChange(userId) {
const user = this.userList.find(item => Number(item.userId) === Number(userId)); const user = this.userList.find(item => Number(item.userId) === Number(userId));
@ -328,7 +307,6 @@ export default {
startTime: null, startTime: null,
endTime: null, endTime: null,
projectDesc: null, projectDesc: null,
groupIds: [],
}; };
this.isView = false; this.isView = false;
this.resetForm('form'); this.resetForm('form');
@ -368,44 +346,11 @@ export default {
}, },
/** 查看项目设备 */ /** 查看项目设备 */
handleViewDevices(row) { handleViewDevices(row) {
const projectId = row.projectId || this.ids[0];
getProject(projectId).then(response => {
const groupIds = (response.data && response.data.groupIds) || [];
if (!groupIds.length) {
this.$message.warning('该项目未关联设备分组');
return;
}
if (groupIds.length === 1) {
this.goDeviceByGroup(groupIds[0]);
return;
}
this.selectableGroupList = groupIds.map(id => {
const group = this.groupList.find(item => Number(item.groupId) === Number(id));
return group || { groupId: id, groupName: '分组' + id };
});
this.selectedGroupId = this.selectableGroupList[0].groupId;
this.groupSelectOpen = true;
});
},
/** 跳转设备管理页并按分组筛选 */
goDeviceByGroup(groupId) {
this.$router.push({ this.$router.push({
path: '/iot/device', path: '/iot/device',
query: { query: { projectId: row.projectId, t: Date.now() }
groupId,
t: Date.now(),
},
}); });
}, },
/** 确认查看所选分组设备 */
confirmViewDevices() {
if (!this.selectedGroupId) {
this.$message.warning('请选择设备分组');
return;
}
this.groupSelectOpen = false;
this.goDeviceByGroup(this.selectedGroupId);
},
/** 编辑 */ /** 编辑 */
handleUpdate(row) { handleUpdate(row) {
this.reset(); this.reset();
@ -471,10 +416,8 @@ export default {
const centerLat = this.pickerLatitude || 35.0; const centerLat = this.pickerLatitude || 35.0;
const zoom = (this.pickerLongitude && this.pickerLatitude) ? 12 : 4; const zoom = (this.pickerLongitude && this.pickerLatitude) ? 12 : 4;
if (this.tiandituMap) { // 天地图 v4 没有 destroy(),先清理旧实例再重建
this.tiandituMap.destroy(); this.resetMap();
this.tiandituMap = null;
}
const map = new T.Map(container); const map = new T.Map(container);
map.centerAndZoom(new T.LngLat(centerLng, centerLat), zoom); map.centerAndZoom(new T.LngLat(centerLng, centerLat), zoom);
@ -484,9 +427,9 @@ export default {
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:'; const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
const tk = DEFAULT_TIANDITU_TILE_KEY; const tk = DEFAULT_TIANDITU_TILE_KEY;
// 底图 // 底图
map.addSubLayer(new T.TileLayer(`${protocol}//t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${tk}`, { subdomains: '01234567' })); map.addLayer(new T.TileLayer(`${protocol}//t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${tk}`, { subdomains: '01234567' }));
// 注记 // 注记
map.addSubLayer(new T.TileLayer(`${protocol}//t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${tk}`, { subdomains: '01234567' })); map.addLayer(new T.TileLayer(`${protocol}//t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${tk}`, { subdomains: '01234567' }));
// 已有坐标则添加标记 // 已有坐标则添加标记
if (this.pickerLongitude && this.pickerLatitude) { if (this.pickerLongitude && this.pickerLatitude) {
@ -514,8 +457,9 @@ export default {
}); });
this.tiandituMap = map; this.tiandituMap = map;
}).catch(() => { }).catch((err) => {
this.$message.error('天地图加载失败,请检查网络'); console.error('天地图加载失败:', err);
this.$message.error('天地图加载失败,请检查网络后重试');
}); });
}, },
/** 逆地理编码:经纬度 → 地址 */ /** 逆地理编码:经纬度 → 地址 */
@ -574,16 +518,27 @@ export default {
} }
this.mapPickerOpen = false; this.mapPickerOpen = false;
}, },
/** 销毁地图实例 */ /** 清理地图实例(天地图 v4 无 destroy,手动清理覆盖物并清空容器) */
disposeMap() { resetMap() {
if (this.tiandituMarker && this.tiandituMap) {
this.tiandituMap.removeOverLay(this.tiandituMarker);
this.tiandituMarker = null;
}
if (this.tiandituMap) { if (this.tiandituMap) {
this.tiandituMap.destroy(); try {
if (typeof this.tiandituMap.clearOverLays === 'function') {
this.tiandituMap.clearOverLays();
}
} catch (e) {
console.warn('清理天地图覆盖物失败:', e);
}
this.tiandituMap = null; this.tiandituMap = null;
} }
const container = this.$refs.mapContainer;
if (container) {
container.innerHTML = '';
}
},
/** 销毁地图实例 */
disposeMap() {
this.tiandituMarker = null;
this.resetMap();
}, },
}, },
}; };

View File

@ -278,7 +278,7 @@ export default {
/** /**
* 弹窗打开时拉取设备列表。必须用 shortList:/iot/device/list 的 SQL 不查 device_type, * 弹窗打开时拉取设备列表。必须用 shortList:/iot/device/list 的 SQL 不查 device_type,
* 返回的 deviceType 恒为 null,前端按 1/2 过滤会清空列表(远程搜索同样会失效)。 * 返回的 deviceType 恒为 null,前端按 1/2 过滤会清空列表(远程搜索同样会失效)。
* shortList 与设备首页一致,含产品 device_type;entrance=1 排除网关子设备类产品(4)。 * shortList 与设备首页一致,含产品 device_type;entrance=2 排除网关子设备类产品(4)。
*/ */
loadDeviceOptions(mergeDevice) { loadDeviceOptions(mergeDevice) {
this.deviceOptions = []; this.deviceOptions = [];
@ -286,7 +286,7 @@ export default {
listDeviceShort({ listDeviceShort({
pageNum: 1, pageNum: 1,
pageSize: 500, pageSize: 500,
entrance: 1, entrance: 2,
showChild: true showChild: true
}).then(res => { }).then(res => {
const map = new Map(); const map = new Map();

View File

@ -669,7 +669,7 @@ export default {
searchDevice(query) { searchDevice(query) {
const keyword = (query || '').trim() const keyword = (query || '').trim()
this.deviceSearchLoading = true this.deviceSearchLoading = true
const base = { pageNum: 1, pageSize: 50, entrance: 1, showChild: true } const base = { pageNum: 1, pageSize: 50, entrance: 2, showChild: true }
const requests = [] const requests = []
if (!keyword) { if (!keyword) {
requests.push(listDeviceShort(base)) requests.push(listDeviceShort(base))