diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ab47065f..9a2dc969 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -35,8 +35,35 @@ "Bash(grep:*)", "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(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/**)" ] } } diff --git a/bin/script/send_modbus_and_register.py b/bin/script/send_modbus_and_register.py index 88c9607b..3e5d226b 100644 --- a/bin/script/send_modbus_and_register.py +++ b/bin/script/send_modbus_and_register.py @@ -8,8 +8,10 @@ TARGET_PORT = 8888 # 请根据实际情况修改 # 元数组 modbus 指令 MODBUS_DATA_HEX = ( - "7e80DFEB502006887e", - "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99" + "7e80D4F8E47AFB347e", + "01031c42310b2f41a3000040ae48850000000042b166ea3fa487e43e3fefd65e78", + "02031c422a945a41b4000040a3f8210000000042a7fd174081531ec08dec058dc0", + "03031c4223b88041970000409c54a30000000042aa4f04bfa6c19fc095539b4f18" ) # 重要-四川远德MQTT推送 (倾角计) @@ -21,6 +23,10 @@ MODBUS_DATA_HEX = ( # "020320413b9fa441acd34dc0024a003fa2a59ac2af34a0c18e17ea413167dbc3f9c7fe6d40", # "0303204139565641ab21ddbff6af333fe26800c2aec626c18685244176ee82c3f9bd7568c8", # "040320413a0d3e41a534d3bf461ccd403d7e00c2ade1e4c0d819c441ce9ca7c3f9a4d260bf" +# 静力水准仪 +# "01031c41aa0c7941d78000402000220000000042b121523fb1fb373eb5668c9cd8", +# "02031c419b24c741e18000400bf3910000000042a83e6840824127c0877958c7d0", +# "03031c418eb64a41cb00003ff8d8130000000042aaaa05bf95093cc090a1de8803" # 01 03 1c 4268 442f 41d3 0000 418b 6685 0000 0000 4026 f4ca c00d 1fd3 c2ad 2a9d 6311 # 从机地址 功能码 字节长度 2个字节 CRC校验 diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java index 07f0f26f..3d891bc4 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/device/DeviceController.java @@ -130,6 +130,16 @@ public class DeviceController extends BaseController { 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)); + } + /** * 查询所有设备简短列表 */ diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/project/ProjectController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/project/ProjectController.java index 9bd1ce1f..5c0037e5 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/project/ProjectController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/project/ProjectController.java @@ -65,22 +65,9 @@ public class ProjectController extends BaseController { @ApiOperation("获取项目详情") public AjaxResult getInfo(@PathVariable("projectId") Long projectId) { Project project = projectService.selectProjectById(projectId); - if (project != null) { - project.setGroupIds(projectService.selectGroupIdsByProjectId(projectId)); - } 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 groupIds 分组ID数组 + * @param deviceIds 设备ID数组 * @return 结果 */ @PreAuthorize("@ss.hasPermi('iot:project:edit')") - @Log(title = "项目分组", businessType = BusinessType.UPDATE) - @PutMapping("/updateProjectGroups") - @ApiOperation("更新项目下的关联分组") - public AjaxResult updateProjectGroups(@RequestParam Long projectId, @RequestBody Long[] groupIds) { - return toAjax(projectService.updateProjectGroups(projectId, groupIds)); + @Log(title = "项目设备", businessType = BusinessType.UPDATE) + @PutMapping("/updateProjectDevices") + @ApiOperation("更新项目下的关联设备") + public AjaxResult updateProjectDevices(@RequestParam Long projectId, @RequestBody Long[] deviceIds) { + return toAjax(projectService.updateProjectDevices(projectId, deviceIds)); } /** diff --git a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/utils/CoordinateTransformUtils.java b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/utils/CoordinateTransformUtils.java index 4f6ed84d..a0a0336a 100644 --- a/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/utils/CoordinateTransformUtils.java +++ b/fastbee-plugs/fastbee-http/src/main/java/com/fastbee/http/utils/CoordinateTransformUtils.java @@ -202,13 +202,10 @@ public class CoordinateTransformUtils { double[] bd = CoordinateTransformUtils.gcj02ToBd09(gcj[0], gcj[1]); CoordinateTransformUtils.printConvert("GCJ-02", gcj[0], gcj[1], "BD-09", bd); - // BD-09 → GCJ-02 - double[] gcjBack = CoordinateTransformUtils.bd09ToGcj02(112.945473,28.234889); - CoordinateTransformUtils.printConvert("BD-09", bd[0], bd[1], "GCJ-02", gcjBack); - - // 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); + // GCJ-02 → WGS-84 112.87,28.12 + double[] GCJ02 = {112.87,28.12}; + double[] wgsBack = CoordinateTransformUtils.gcj02ToWgs84(GCJ02[0], GCJ02[1]); + CoordinateTransformUtils.printConvert("GCJ-02", GCJ02[0], GCJ02[1], "WGS-84", wgsBack); // 直接计算 WGS-84 → BD-09 double[] directBd = CoordinateTransformUtils.wgs84ToBd09(wgsLng, wgsLat); diff --git a/fastbee-server/base-server/src/main/java/com/fastbee/base/session/Session.java b/fastbee-server/base-server/src/main/java/com/fastbee/base/session/Session.java index a7986c4b..63a28122 100644 --- a/fastbee-server/base-server/src/main/java/com/fastbee/base/session/Session.java +++ b/fastbee-server/base-server/src/main/java/com/fastbee/base/session/Session.java @@ -178,15 +178,13 @@ public class Session { /*设备端注册*/ public void register(Message message) { - register(message.getClientId(), message); - } - - public void register(String clientId, Message message) { + String clientId = message.getClientId(); //已经注册,不再发送注册包数据 if (isRegistered()) { return; } if (clientId == null) { + log.error("messageId:{},serNo:{},channelId:{}", message.getMessageId(), message.getSerNo(), message.getChannelId()); throw new ServiceException("客户端注册clientId不能为空"); } this.clientId = clientId.toUpperCase(); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java index 16ec4ef2..e1369703 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Device.java @@ -227,6 +227,19 @@ public class Device extends BaseEntity { @TableField(exist = false) private String transport; + /** + * 项目设备选择查询模式(非数据库字段) + */ + @ApiModelProperty("项目设备选择查询模式") + @TableField(exist = false) + private Boolean projectDeviceMode; + + /** + * 所属项目ID + */ + @ApiModelProperty("所属项目ID") + private Long projectId; + /** * IMEI(蜂窝模块设备标识) */ @@ -250,12 +263,12 @@ public class Device extends BaseEntity { private Boolean showChild; @Transient - @ApiModelProperty("查询入口(1=设备管理列表 2=数据中心 3=全部设备)") + @ApiModelProperty("查询入口(1=设备管理列表(需要查看更新时间) 2=数据联动、项目管理、设备绑定 3=全部设备)") @TableField(exist = false) private Integer entrance; - @TableField(exist = false) - private List subDeviceList; +// @TableField(exist = false) +// private List subDeviceList; /** * 删除标志(0代表存在 2代表删除) @@ -289,9 +302,4 @@ public class Device extends BaseEntity { @TableField(exist = false) private Boolean canConfigPoll = false; - - public static long getSerialVersionUID() { - return serialVersionUID; - } - } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Project.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Project.java index c95f34d2..cb23b337 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Project.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Project.java @@ -121,12 +121,6 @@ public class Project extends BaseEntity { @ApiModelProperty("删除标志") private String delFlag; - /** - * 关联分组ID数组(用于项目-分组关系管理) - */ - @ApiModelProperty("关联分组ID数组") - private Long[] groupIds; - /** * 项目关联设备总数 */ @@ -265,14 +259,6 @@ public class Project extends BaseEntity { this.delFlag = delFlag; } - public Long[] getGroupIds() { - return groupIds; - } - - public void setGroupIds(Long[] groupIds) { - this.groupIds = groupIds; - } - public Integer getDeviceCount() { return deviceCount; } @@ -320,7 +306,6 @@ public class Project extends BaseEntity { .append("updateBy", getUpdateBy()) .append("updateTime", getUpdateTime()) .append("remark", getRemark()) - .append("groupIds", getGroupIds()) .append("deviceCount", getDeviceCount()) .append("onlineCount", getOnlineCount()) .append("offlineCount", getOfflineCount()) diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/DeviceMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/DeviceMapper.java index e2ea2801..670c9a2a 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/DeviceMapper.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/DeviceMapper.java @@ -145,6 +145,22 @@ public interface DeviceMapper extends BaseMapperX { */ public List 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 { */ public List 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); + } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/ProjectMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/ProjectMapper.java index 324795e3..397ca3a0 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/ProjectMapper.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/ProjectMapper.java @@ -33,12 +33,12 @@ public interface ProjectMapper { public List selectProjectList(Project project); /** - * 通过项目ID查询关联的分组ID数组 + * 通过项目ID查询关联的设备ID数组 * * @param projectId 项目ID - * @return 分组ID数组 + * @return 设备ID数组 */ - public Long[] selectGroupIdsByProjectId(Long projectId); + public Long[] selectDeviceIdsByProjectId(Long projectId); /** * 根据负责人ID查询项目列表 @@ -104,23 +104,6 @@ public interface ProjectMapper { */ 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); - /** * 批量删除项目 * diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceAllShortOutput.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceAllShortOutput.java index b5e94a10..9b5798dd 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceAllShortOutput.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceAllShortOutput.java @@ -34,6 +34,11 @@ public class DeviceAllShortOutput { */ private Integer deviceType; + /** + * 项目ID + */ + private Long projectId; + /** * 用户昵称 */ @@ -203,6 +208,14 @@ public class DeviceAllShortOutput { this.deviceType = deviceType; } + public Long getProjectId() { + return projectId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + public String getUserName() { return userName; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceShortOutput.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceShortOutput.java index 606d9eb7..a87ee53e 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceShortOutput.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/DeviceShortOutput.java @@ -57,6 +57,20 @@ public class DeviceShortOutput { @Excel(name = "设备编号") private String serialNumber; + /** + * 项目ID + */ + private Long projectId; + + /** + * 经度 + */ + private BigDecimal longitude; + /** + * 纬度 + */ + private BigDecimal latitude; + /** * IMEI(蜂窝模块设备标识) */ @@ -419,6 +433,30 @@ public class DeviceShortOutput { 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() { return imei; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java index 0100cd88..30ea3829 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IDeviceService.java @@ -147,6 +147,14 @@ public interface IDeviceService { */ public List selectDeviceShortList(Device device); + /** + * 查询当前用户可见设备统计(与 shortList 相同范围) + * + * @param device 设备筛选条件 + * @return 设备统计 + */ + public DeviceStatistic selectUserDeviceStatistic(Device device); + /** * 新增设备 * diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IProjectService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IProjectService.java index 45c9e901..cb5f49b9 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IProjectService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IProjectService.java @@ -30,12 +30,12 @@ public interface IProjectService { public List selectProjectList(Project project); /** - * 通过项目ID查询关联的分组ID数组 + * 通过项目ID查询关联的设备ID数组 * * @param projectId 项目ID - * @return 分组ID数组 + * @return 设备ID数组 */ - public Long[] selectGroupIdsByProjectId(Long projectId); + public Long[] selectDeviceIdsByProjectId(Long projectId); /** * 根据负责人ID查询项目列表 @@ -102,13 +102,13 @@ public interface IProjectService { public int updateProject(Project project); /** - * 更新项目关联的分组 + * 更新项目关联的设备 * * @param projectId 项目ID - * @param groupIds 分组ID数组 + * @param deviceIds 设备ID数组 * @return 结果 */ - public int updateProjectGroups(Long projectId, Long[] groupIds); + public int updateProjectDevices(Long projectId, Long[] deviceIds); /** * 批量删除项目 diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java index a35fb475..68558653 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java @@ -996,6 +996,7 @@ public class DeviceServiceImpl implements IDeviceService { */ @Override public List selectDeviceShortList(Device device) { + long startTime = System.currentTimeMillis(); List list = deviceMapper.selectDeviceShortList(device); if (CollectionUtils.isEmpty(list)) { return new ArrayList<>(); @@ -1018,15 +1019,41 @@ public class DeviceServiceImpl implements IDeviceService { } } fillShortListLatestReportTime(list, device.getEntrance()); + log.warn("当前selectDeviceShortList查询接口耗时: {}", System.currentTimeMillis() - startTime); 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)。 * 网关设备自身往往无属性上报,数据在子设备 SN 的缓存中,需合并子设备的 ts。 */ private void fillShortListLatestReportTime(List list, Integer entrance) { - if (CollectionUtils.isEmpty(list) && !Integer.valueOf(1).equals(entrance)) { + if (CollectionUtils.isEmpty(list) || !Integer.valueOf(1).equals(entrance)) { return; } for (DeviceShortOutput row : list) { @@ -2186,8 +2213,10 @@ public class DeviceServiceImpl implements IDeviceService { @Override public List listTerminalUser(Device device) { + long startTime = System.currentTimeMillis(); List list = deviceMapper.listTerminalUser(device); fillShortListLatestReportTime(list, device.getEntrance()); + log.warn("当前listTerminalUser查询接口耗时: {}", System.currentTimeMillis() - startTime); return list; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ProjectServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ProjectServiceImpl.java index e34f0f37..8360dc5d 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ProjectServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ProjectServiceImpl.java @@ -4,6 +4,7 @@ import com.fastbee.common.utils.DateUtils; import com.fastbee.common.core.domain.entity.SysUser; import com.fastbee.iot.domain.Device; import com.fastbee.iot.domain.Project; +import com.fastbee.iot.mapper.DeviceMapper; import com.fastbee.iot.mapper.ProjectMapper; import com.fastbee.iot.service.IProjectService; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +29,9 @@ public class ProjectServiceImpl implements IProjectService { @Autowired private ProjectMapper projectMapper; + @Autowired + private DeviceMapper deviceMapper; + /** * 查询项目 * @@ -58,14 +62,14 @@ public class ProjectServiceImpl implements IProjectService { } /** - * 通过项目ID查询关联的分组ID数组 + * 通过项目ID查询关联的设备ID数组 * * @param projectId 项目ID - * @return 分组ID数组 + * @return 设备ID数组 */ @Override - public Long[] selectGroupIdsByProjectId(Long projectId) { - return projectMapper.selectGroupIdsByProjectId(projectId); + public Long[] selectDeviceIdsByProjectId(Long projectId) { + return projectMapper.selectDeviceIdsByProjectId(projectId); } /** @@ -171,18 +175,11 @@ public class ProjectServiceImpl implements IProjectService { project.setCreateBy(getLoginUser().getUsername()); project.setDelFlag("0"); - int result = projectMapper.insertProject(project); - - // 插入项目-分组关系 - if (project.getGroupIds() != null && project.getGroupIds().length > 0) { - projectMapper.insertProjectGroups(project.getProjectId(), project.getGroupIds()); - } - - return result; + return projectMapper.insertProject(project); } /** - * 修改项目(含分组关联更新) + * 修改项目 * * @param project 项目 * @return 结果 @@ -192,38 +189,30 @@ public class ProjectServiceImpl implements IProjectService { public int updateProject(Project project) { project.setUpdateTime(DateUtils.getNowDate()); project.setUpdateBy(getLoginUser().getUsername()); - int rows = 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; + return projectMapper.updateProject(project); } /** - * 更新项目关联的分组(事务管理) + * 更新项目关联的设备(事务管理) * * @param projectId 项目ID - * @param groupIds 分组ID数组 + * @param deviceIds 设备ID数组 * @return 结果 */ @Override @Transactional(rollbackFor = Exception.class) - public int updateProjectGroups(Long projectId, Long[] groupIds) { - projectMapper.deleteProjectGroupsByProjectId(projectId); - if (groupIds != null && groupIds.length > 0) { - return projectMapper.insertProjectGroups(projectId, groupIds); + public int updateProjectDevices(Long projectId, Long[] deviceIds) { + // 先清除项目下不在新列表中的设备归属 + deviceMapper.clearDeviceProjectExcept(projectId, deviceIds); + // 再将新列表中的设备归属到本项目(覆盖式转移) + if (deviceIds != null && deviceIds.length > 0) { + deviceMapper.updateDeviceProjectByIds(projectId, deviceIds); } return 1; } /** - * 批量删除项目(同时清理关联分组) + * 批量删除项目 * * @param projectIds 需要删除的项目主键集合 * @return 结果 @@ -231,15 +220,12 @@ public class ProjectServiceImpl implements IProjectService { @Override @Transactional(rollbackFor = Exception.class) public int deleteProjectByIds(Long[] projectIds) { - // 先清理关联分组关系 - for (Long projectId : projectIds) { - projectMapper.deleteProjectGroupsByProjectId(projectId); - } + deviceMapper.clearDeviceProjectByProjectIds(projectIds); return projectMapper.deleteProjectByIds(projectIds); } /** - * 删除项目信息(同时清理关联分组) + * 删除项目信息 * * @param projectId 项目主键 * @return 结果 @@ -247,7 +233,7 @@ public class ProjectServiceImpl implements IProjectService { @Override @Transactional(rollbackFor = Exception.class) public int deleteProjectById(Long projectId) { - projectMapper.deleteProjectGroupsByProjectId(projectId); + deviceMapper.clearDeviceProjectByProjectIds(new Long[]{projectId}); return projectMapper.deleteProjectById(projectId); } diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml index 2a895354..b55fa1a8 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/DeviceMapper.xml @@ -38,6 +38,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + update iot_device set project_id = #{projectId} where device_id in + + #{id} + + + + + + update iot_device set project_id = null where project_id = #{projectId} + + and device_id not in + + #{id} + + + + + + update iot_device set project_id = null where project_id in + + #{id} + + + diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ProjectMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ProjectMapper.xml index cb79259b..7ae07ed4 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ProjectMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ProjectMapper.xml @@ -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.device_id IS NOT NULL AND d.status != 3 THEN 1 ELSE 0 END) AS offline_count FROM iot_project p - LEFT JOIN iot_project_group pg ON p.project_id = pg.project_id - 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' + LEFT JOIN iot_device d ON d.project_id = p.project_id AND d.del_flag = '0' p.del_flag = '0' and p.project_name like concat('%', #{projectName}, '%') @@ -79,8 +77,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where project_id = #{projectId} and del_flag = '0' - + select device_id from iot_device where project_id=#{projectId} and del_flag='0' 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 - INNER JOIN iot_device_group dg ON d.device_id = dg.device_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 + INNER JOIN iot_project p ON d.project_id = p.project_id WHERE p.responsible_user_id = #{userId} AND p.del_flag='0' AND d.del_flag='0' @@ -191,15 +185,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - - - insert into iot_project_group (project_id,group_id) - values - - (#{projectId},#{groupId}) - - - update iot_project @@ -226,10 +211,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where project_id = #{projectId} - - delete from iot_project_group where project_id=#{projectId} - - update iot_project set del_flag = '2' where project_id in @@ -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 offline_count FROM iot_project p - LEFT JOIN iot_project_group pg ON p.project_id = pg.project_id - 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' + LEFT JOIN iot_device d ON d.project_id = p.project_id AND d.del_flag = '0' WHERE p.project_id = #{projectId} AND p.del_flag = '0' GROUP BY p.project_id diff --git a/sql/iot_project_device_migration.sql b/sql/iot_project_device_migration.sql new file mode 100644 index 00000000..355af707 --- /dev/null +++ b/sql/iot_project_device_migration.sql @@ -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 表保留,继续用于独立的设备分组管理功能 +-- ======================================== diff --git a/sql/iot_project_tables.sql b/sql/iot_project_tables.sql index 1922950f..cf2381c6 100644 --- a/sql/iot_project_tables.sql +++ b/sql/iot_project_tables.sql @@ -1,10 +1,11 @@ -- ======================================== -- FastBee项目管理数据库表创建脚本 -- 创建日期: 2026-06-28 --- 描述: 创建项目表和项目-分组关系表,实现User→Project→Group→Device层级 +-- 更新日期: 2026-08-01 +-- 描述: 创建项目表,实现User→Project→Device直接关联 -- ======================================== --- 1. 创建项目表(iot_project) +-- 创建项目表(iot_project) -- 实现 User → Project 层级(通过responsible_user_id字段) CREATE TABLE `iot_project` ( `project_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '项目ID', @@ -33,34 +34,26 @@ CREATE TABLE `iot_project` ( KEY `idx_create_time` (`create_time`) ) 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表保持不变(现有分组管理逻辑不受影响) --- 2. iot_device_user表保持不变(设备归属关系独立) --- 3. 项目-分组关系是可选的(未关联项目的分组保持现有功能) +-- 1. iot_group / iot_device_group 表保持不变(独立的设备分组管理功能) +-- 2. iot_device_user 表保持不变(设备归属关系独立) -- -- 用户查询设备的两种路径: --- 路径1:查询用户负责的项目设备(新增功能) --- SELECT DISTINCT d.* FROM iot_device d --- INNER JOIN iot_device_group dg ON d.device_id = dg.device_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 +-- 路径1:查询用户负责的项目设备(项目维度) +-- SELECT d.* FROM iot_device d +-- INNER JOIN iot_project p ON d.project_id = p.project_id -- WHERE p.responsible_user_id = #{userId} AND p.del_flag='0' AND d.del_flag='0' -- --- 路径2:查询设备归属用户的设备(现有逻辑) +-- 路径2:查询设备归属用户的设备(用户维度) -- SELECT d.* FROM iot_device d -- INNER JOIN iot_device_user du ON d.device_id = du.device_id -- WHERE du.user_id = #{userId} AND d.del_flag='0' diff --git a/yack-web/src/api/iot/device.js b/yack-web/src/api/iot/device.js index 874f4fea..d387eb78 100644 --- a/yack-web/src/api/iot/device.js +++ b/yack-web/src/api/iot/device.js @@ -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) { return request({ diff --git a/yack-web/src/api/iot/project.js b/yack-web/src/api/iot/project.js index 48421f44..3abfc6da 100644 --- a/yack-web/src/api/iot/project.js +++ b/yack-web/src/api/iot/project.js @@ -17,14 +17,24 @@ export function getProject(projectId) { }) } -// 查询项目下的关联分组ID数组 -export function getGroupIds(projectId) { +// 查询项目下的关联设备ID数组 +export function getProjectDeviceIds(projectId) { return request({ - url: '/iot/project/getGroupIds/' + projectId, + url: '/iot/project/deviceIds/' + projectId, method: 'get' }) } +// 更新项目下的关联设备 +export function updateProjectDevices(projectId, deviceIds) { + return request({ + url: '/iot/project/updateProjectDevices', + method: 'put', + params: { projectId }, + data: deviceIds + }) +} + // 新增项目 export function addProject(data) { 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) { return request({ diff --git a/yack-web/src/utils/tdtMap.js b/yack-web/src/utils/tdtMap.js index b8abaa51..7c23d156 100644 --- a/yack-web/src/utils/tdtMap.js +++ b/yack-web/src/utils/tdtMap.js @@ -1,10 +1,11 @@ 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; export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) { - if (window.T) { + if (window.T && window.T.Map) { return Promise.resolve(window.T); } if (tiandituLoading) { @@ -12,11 +13,10 @@ export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) { } tiandituLoading = new Promise((resolve, reject) => { + // 移除之前可能加载失败的脚本,避免复用失效的 script 标签 const existingScript = document.getElementById('tianditu-js-api'); if (existingScript) { - existingScript.addEventListener('load', () => resolve(window.T)); - existingScript.addEventListener('error', reject); - return; + existingScript.remove(); } const script = document.createElement('script'); @@ -25,14 +25,18 @@ export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) { const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:'; script.src = `${protocol}//api.tianditu.gov.cn/api?v=4.0&tk=${tk}`; script.onload = () => { - if (window.T) { + if (window.T && window.T.Map) { resolve(window.T); } else { reject(new Error('天地图脚本已加载,但未找到 T 对象')); } }; - script.onerror = reject; + script.onerror = (err) => reject(err); document.head.appendChild(script); + }).catch((err) => { + // 加载失败时清空缓存,允许下次重试 + tiandituLoading = null; + throw err; }); return tiandituLoading; diff --git a/yack-web/src/views/bigScreen/home.scss b/yack-web/src/views/bigScreen/home.scss index 2a41e3d1..aa3b874a 100644 --- a/yack-web/src/views/bigScreen/home.scss +++ b/yack-web/src/views/bigScreen/home.scss @@ -16,7 +16,7 @@ .cockpit-page { width: 100vw; height: 100vh; - background: #020918; + background: #010820; } .cockpit-page--map-view .cockpit-bg { @@ -24,69 +24,8 @@ 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 { - height: calc(100% - 54px); + height: calc(100% - 84px); } .cockpit-bg { @@ -98,7 +37,7 @@ background: 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%), - linear-gradient(135deg, #030814 0%, #071a46 42%, #030918 100%); + linear-gradient(135deg, #010820 0%, #092156 42%, #010820 100%); overflow: hidden; &::before { @@ -120,125 +59,319 @@ height: 100%; } -.cockpit-header--new { +.cockpit-header { position: relative; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; height: 76px; 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; - top: -10px; left: 50%; - z-index: 4; - width: 580px; - height: 72px; + top: 50%; + z-index: 3; display: flex; align-items: center; justify-content: center; - color: #f7fbff; + height: 62px; + padding: 0 52px; + transform: translate(-50%, -50%); cursor: pointer; 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%); + 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); - &::before, - &::after { - position: absolute; - content: ''; + &__inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + pointer-events: none; } - &::before { - left: 34px; - right: 34px; - bottom: 8px; - height: 2px; - background: linear-gradient(90deg, transparent, rgba(94, 232, 255, .9), transparent); + &__cn { + color: #f7fbff; + font-size: 26px; + font-weight: 900; + letter-spacing: 5px; + line-height: 1; + white-space: nowrap; + text-shadow: 0 0 18px rgba(87, 220, 255, .68); } - &::after { - inset: 8px 40px auto; - height: 1px; - background: linear-gradient(90deg, transparent, rgba(125, 222, 255, .7), transparent); + &__en { + color: rgba(161, 212, 255, .75); + font-size: 10px; + letter-spacing: 3px; + text-transform: uppercase; + white-space: nowrap; } } -.header-title-console__text { - font-size: 34px; - font-weight: 900; - letter-spacing: 4px; - line-height: 1; - text-shadow: 0 0 18px rgba(87, 220, 255, .68); +// 右侧时间 + 用户 +.header-aside { + display: flex; + align-items: center; + gap: 20px; + z-index: 2; } -.header-wing { - position: absolute; - top: 0; - z-index: 3; - width: 500px; - height: 60px; - border: 0; - color: #eef8ff; - cursor: pointer; - font-size: 27px; - font-weight: 800; - letter-spacing: 2px; - background: - 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); - box-shadow: inset 0 -12px 20px rgba(5, 14, 66, .34); - transition: color .18s ease, filter .18s ease, transform .18s ease; +.header-clock { + display: flex; + 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; + text-shadow: 0 0 12px rgba(86, 214, 255, .5); + } + + &__week { + color: #a6bde8; + font-size: 13px; + } } -.header-wing i { - margin-right: 12px; - color: #2ecbff; - font-size: 32px; - vertical-align: -3px; +.header-user { + display: flex; + align-items: center; + + &__trigger { + display: flex; + align-items: center; + gap: 10px; + 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; + + &:hover { + border-color: rgba(104, 224, 255, .6); + background: rgba(12, 45, 110, .55); + } + } + + &__avatar { + width: 34px; + height: 34px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(69, 204, 255, .6); + box-shadow: 0 0 10px rgba(32, 150, 255, .4); + } + + &__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; + } } -.header-wing:hover, -.header-wing.active { - color: #fff; - filter: brightness(1.12); - transform: translateY(-1px); +// 下拉菜单样式(全局样式,通过 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; + } } -.header-wing--left { - left: 182px; - padding-right: 132px; - clip-path: polygon(0 0, 88% 0, 100% 100%, 8% 100%); -} +// 退出登录确认框(科技风,通过 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; -.header-wing--right { - right: 182px; - padding-left: 132px; - clip-path: polygon(12% 0, 100% 0, 92% 100%, 0 100%); -} + .el-message-box__header { + padding: 16px 20px 8px !important; + border-bottom: 1px solid rgba(69, 204, 255, 0.2) !important; + } -.admin-stealth { - position: absolute; - top: 12px; - right: 22px; - z-index: 8; - width: 34px; - height: 34px; - border: 1px solid rgba(99, 184, 255, .26); - color: rgba(161, 212, 255, .46); - cursor: pointer; - background: rgba(4, 16, 56, .3); - transition: color .18s ease, border-color .18s ease, background .18s ease; -} + .el-message-box__title { + color: #dff7ff !important; + font-weight: 700; + letter-spacing: 1px; + display: flex; + align-items: center; + gap: 6px; -.admin-stealth:hover { - color: #8fe8ff; - border-color: rgba(104, 224, 255, .64); - background: rgba(7, 35, 95, .72); + &::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; + border-color: rgba(104, 224, 255, 0.64); + 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 { @@ -306,7 +439,7 @@ overflow: hidden; border: 1px solid rgba(46, 152, 255, .46); 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%); box-shadow: inset 0 0 28px rgba(28, 142, 255, .18), @@ -353,7 +486,7 @@ margin-right: 8px; content: ''; 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); } } @@ -380,7 +513,7 @@ padding: 12px 8px; box-sizing: border-box; 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 { @@ -422,7 +555,7 @@ align-items: center; justify-content: center; 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-height: 0; 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); overflow: hidden; } @@ -619,6 +752,32 @@ font-size: 12px; border: 1px solid rgba(62, 179, 255, .3); 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 { @@ -686,7 +845,7 @@ background: 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%), - 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 { @@ -704,7 +863,7 @@ color: #9ec9e8; font-size: 12px; border: 1px solid rgba(70, 195, 255, .28); - background: rgba(3, 18, 56, .72); + background: rgba(1, 8, 32, .72); } .selected-project { @@ -715,8 +874,9 @@ min-width: 280px; padding: 14px 16px; 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); + pointer-events: none; } .selected-project--view { @@ -796,6 +956,12 @@ 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-meta { display: flex; @@ -907,7 +1073,7 @@ th.el-table__cell { color: #bde2ff; - background: rgba(9, 39, 93, .86); + background: rgba(9, 33, 86, .86); border-color: rgba(82, 168, 255, .2); } @@ -930,3 +1096,58 @@ top: 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; /* 星期 */ +} diff --git a/yack-web/src/views/bigScreen/home.vue b/yack-web/src/views/bigScreen/home.vue index f7b5bc8d..6bcff7f2 100644 --- a/yack-web/src/views/bigScreen/home.vue +++ b/yack-web/src/views/bigScreen/home.vue @@ -3,21 +3,60 @@
Loading...
-
- -
-
岩安测控大数据中心
+
+ + + + +
+
+ 岩安测控大数据中心 + YAN'AN IOT BIG DATA CENTER +
+
+ + +
+
+ {{ dateYear }} + {{ dateDay }} + {{ dateWeek }} +
+ +
+ avatar + {{ headerUserName }} + +
+ + + 个人中心 + + + 修改密码 + + + 管理后台 + + + 退出登录 + + +
- -
@@ -32,23 +71,13 @@
-
+
人员设备视图 当前用户
-
-
- {{ userDeviceCount }} - 关联设备 -
-
- {{ alarmCount }} - 告警数量 -
-
-
+
{{ device.deviceName || device.serialNumber }}
@@ -58,13 +87,17 @@
暂无关联设备
+
+ {{ deviceLoadingMore ? '加载中...' : '加载更多' }} +
滚动预警
-
+
{{ index + 1 }}
{{ alarm.deviceName || alarm.serialNumber || '未知设备' }}
@@ -78,11 +111,12 @@
+
项目 / 设备分布
@@ -93,6 +127,10 @@ 运行项目 完结项目
+
@@ -125,7 +163,13 @@ {{ projectList.length }} 个
-
+
{{ project.name }} @@ -184,9 +228,11 @@
分析条件
- +
测点 / 物模型
-
+
{{ model.modelName || model.identifier }} {{ model.identifier }}
@@ -239,6 +289,7 @@
+
@@ -248,39 +299,64 @@ import vueSeamlessScroll from 'vue-seamless-scroll'; import drawMixin from '../../utils/bigScreen/drawMixin'; import { formatTime } from '../../utils/bigScreen/index.js'; import { loadMap } from '@/utils/map'; +import { DEFAULT_TIANDITU_TILE_KEY } from '@/utils/tdtMap'; import chinaMapJson from '@/assets/echarts-map-json/china.json'; import { getToken, getUserId } from '@/utils/auth'; -import { listAllDeviceShort, getDeviceStatistic, listThingsModel } from '@/api/iot/device'; -import { listGroup, getDeviceIds } from '@/api/iot/group'; +import { getUserProfile } from '@/api/system/user'; +import { listDeviceShort, getDeviceShortStatistic, getDeviceStatistic, listThingsModel } from '@/api/iot/device'; import { listAlertLog } from '@/api/iot/alertLog'; -import { listDeviceUser } from '@/api/iot/deviceuser'; 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 USER_DEVICE_PAGE_SIZE = 10; + export default { name: 'BigScreenHome', - components: { vueSeamlessScroll }, + components: { vueSeamlessScroll, personalCenter }, mixins: [drawMixin], data() { return { activeMode: this.resolveModeByRoute(), loading: true, analysisLoading: false, + infoVisible: false, + pwdVisible: false, + headerUser: {}, dateDay: '', dateYear: '', dateWeek: '', timing: null, weekday: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'], deviceStatistic: {}, - deviceList: [], - groupList: [], + projectDeviceList: [], realProjectList: [], - groupDeviceMap: {}, alarmList: [], userDeviceList: [], userDeviceStatsData: {}, + userDeviceTotal: 0, + userDevicePageNum: 1, + deviceLoadingMore: false, + projectDeviceVisible: 8, + analysisDeviceOptions: [], + selectedAnalysisDevice: null, + analysisDeviceSearchLoading: false, selectedProject: null, projectChart: null, deviceChart: null, @@ -291,6 +367,9 @@ export default { satelliteLayer: null, satelliteLabelLayer: null, satelliteMapLoading: false, + currentProvince: null, + provinceMapJson: null, + pendingProjectToFocus: null, identifierList: [], analysisLineList: [], analysisQuery: { @@ -313,16 +392,31 @@ export default { isScale() { 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() { return getUserId(); }, 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() { - return Math.max(this.deviceList.length - this.onlineCount, 0); + return Math.max(this.activeDeviceList.length - this.onlineCount, 0); }, alarmCount() { + if (this.selectedProject) return this.activeAlarmList.length; return Number(this.deviceStatistic.alertCount || this.alarmList.length || 0); }, warningProjectCount() { @@ -339,16 +433,29 @@ export default { runningProjectCount() { return Math.max(this.projectList.length - this.warningProjectCount - this.completedProjectCount, 0); }, - userDeviceCount() { - if (Object.prototype.hasOwnProperty.call(this.userDeviceStatsData, 'totalDevices')) { - return Number(this.userDeviceStatsData.totalDevices || 0); + totalDeviceCount() { + if (this.selectedProject) return this.projectDeviceList.length; + 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() { - return this.realProjectList.length - ? '项目数据来自 iot_project 与项目分组关系' - : '当前未配置 iot_project,暂按设备分组聚合展示'; + return this.selectedProject ? `${this.selectedProject.name}设备分布` : (this.realProjectList.length ? '项目设备数据来自 iot_device.project_id' : '当前未配置项目'); }, overviewMetrics() { return [ @@ -356,54 +463,47 @@ export default { { key: 'running', label: '运行项目', value: this.runningProjectCount }, { key: 'warning', label: '预警项目', value: this.warningProjectCount }, { key: 'completed', label: '完结项目', value: this.completedProjectCount }, - { key: 'device', label: '设备总数', value: this.deviceList.length || this.deviceStatistic.deviceCount || 0 }, - { key: 'online', label: '在线设备', value: this.onlineCount || this.deviceStatistic.deviceOnlineCount || 0 }, - { key: 'offline', label: '离线设备', value: this.offlineCount }, + { key: 'device', label: '设备总数', value: this.totalDeviceCount }, + { key: 'online', label: '在线设备', value: this.onlineDeviceCount }, + { key: 'offline', label: '离线设备', value: this.offlineDeviceCount }, { key: 'alarm', label: '告警数量', value: this.alarmCount }, ]; }, projectList() { - if (this.realProjectList.length > 0) { - 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, - }), - ]; + return this.realProjectList.map((project, index) => this.normalizeProject(project, index)); }, projectPreview() { return this.projectList.slice(0, 8); }, userDevicePreview() { - const directDevices = this.userDeviceList - .map((rel) => this.deviceList.find((device) => String(device.deviceId) === String(rel.deviceId))) - .filter(Boolean); - return (directDevices.length ? directDevices : this.deviceList).slice(0, 8); + if (this.selectedProject) return this.projectDeviceList.slice(0, this.projectDeviceVisible); + return this.userDeviceList; + }, + 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() { return this.projectList.filter((item) => Number(item.lng) && Number(item.lat)); }, analysisTitle() { - const device = this.deviceList.find((item) => item.deviceId === this.analysisQuery.deviceId); + const device = this.selectedAnalysisDevice; if (!device) return '未选择设备'; return device.deviceName || device.serialNumber; }, @@ -422,7 +522,7 @@ export default { }); }); }); - return rows.slice(0, 200); + return rows.slice(0, 100); }, }, watch: { @@ -450,6 +550,7 @@ export default { this.$nextTick(() => this.calcRate()); this.timeFn(); this.loadCockpitData(); + this.fetchHeaderUser(); }, beforeDestroy() { clearInterval(this.timing); @@ -491,8 +592,31 @@ export default { } }); }, - goAdmin() { - this.$router.push({ path: '/index' }).catch(() => {}); + handleAdminCommand(command) { + if (command === 'admin') { + 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() { if (this.$route.path !== '/bigscreen') { @@ -515,6 +639,14 @@ export default { setTime(); this.timing = setInterval(setTime, 1000); }, + fetchHeaderUser() { + if (!getToken() && this.$route.query.share) return; + getUserProfile() + .then((res) => { + this.headerUser = res.data || {}; + }) + .catch(() => {}); + }, async loadCockpitData() { this.loading = true; if (!getToken() && this.$route.query.share) { @@ -528,14 +660,11 @@ export default { try { await Promise.all([ this.fetchDeviceStatistic(), - this.fetchDevices(), - this.fetchGroups(), this.fetchAlarms(), this.fetchUserDevices(), this.fetchProjects(), this.fetchUserDeviceStats() ]); - await this.fetchGroupDevices(); this.setDefaultAnalysisDevice(); } finally { this.loading = false; @@ -552,24 +681,6 @@ export default { }) .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() { return listAlertLog({ pageNum: 1, pageSize: 20 }) .then((res) => { @@ -580,15 +691,41 @@ export default { }); }, fetchUserDevices() { - if (!this.currentUserId) return Promise.resolve(); - return listDeviceUser({ userId: this.currentUserId, pageNum: 1, pageSize: 9999 }) + this.userDevicePageNum = 1; + this.userDeviceTotal = 0; + return listDeviceShort({ pageNum: 1, pageSize: USER_DEVICE_PAGE_SIZE, showChild: true, entrance: 2 }) .then((res) => { this.userDeviceList = res.rows || []; + this.userDeviceTotal = Number(res.total || 0); }) .catch(() => { 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() { const query = { pageNum: 1, pageSize: 9999 }; if (this.currentUserId) query.responsibleUserId = this.currentUserId; @@ -601,8 +738,7 @@ export default { }); }, fetchUserDeviceStats() { - if (!this.currentUserId) return Promise.resolve(); - return getUserDeviceStats(this.currentUserId) + return getDeviceShortStatistic({ showChild: true, entrance: 2 }) .then((res) => { if (res.code === 200) { this.userDeviceStatsData = res.data || {}; @@ -629,48 +765,38 @@ export default { 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() { - if (!this.analysisQuery.deviceId && this.deviceList.length) { - this.analysisQuery.deviceId = this.deviceList[0].deviceId; - this.handleAnalysisDeviceChange(this.analysisQuery.deviceId); + if (this.analysisQuery.deviceId) return; + const devices = this.selectedProject ? this.projectDeviceList : this.userDeviceList; + 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() { @@ -722,8 +848,8 @@ export default { type: 'bar', barWidth: 28, data: [ - { value: this.onlineCount, itemStyle: { color: '#18f2a3' } }, - { value: this.offlineCount, itemStyle: { color: '#4098ff' } }, + { value: this.onlineDeviceCount, itemStyle: { color: '#18f2a3' } }, + { value: this.offlineDeviceCount, itemStyle: { color: '#4098ff' } }, { value: this.alarmCount, itemStyle: { color: '#ff5574' } }, ], }, @@ -744,9 +870,16 @@ export default { this.$echarts.registerMap('china', chinaMapJson); this[chartKey] = this.$echarts.init(mapRef); this[chartKey].on('click', (params) => { - if (params.seriesType !== 'effectScatter') return; - const project = this.projectList.find((item) => String(item.id) === String(params.data.id)); - if (project) this.selectProject(project); + // 点击散点(项目) + if (params.seriesType === 'effectScatter') { + const project = this.projectList.find((item) => String(item.id) === String(params.data.id)); + if (project) this.selectProject(project); + return; + } + // 点击省份区域 + if (params.componentType === 'geo' && params.name) { + this.loadProvinceMap(params.name); + } }); } const points = this.mapPoints.map((project) => ({ @@ -757,7 +890,12 @@ export default { statusText: project.statusText, 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( { backgroundColor: 'transparent', @@ -771,10 +909,10 @@ export default { }, }, geo: { - map: 'china', + map: this.currentProvince || 'china', roam: false, layoutCenter: layoutConfig.layoutCenter, - layoutSize: layoutConfig.layoutSize, + layoutSize: this.currentProvince ? '90%' : layoutConfig.layoutSize, aspectScale: 0.82, label: { show: true, @@ -800,7 +938,7 @@ export default { name: '项目分布', type: 'effectScatter', coordinateSystem: 'geo', - data: points, + data: displayPoints, symbolSize: (value) => Math.max(13, Math.min(30, 12 + Number(value[2] || 0) * 2)), rippleEffect: { brushType: 'stroke', scale: 4.5 }, label: { @@ -844,20 +982,32 @@ export default { attributionControl: false, worldCopyJump: true, }); - this.satelliteLayer = L.tileLayer('https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', { - subdomains: ['1', '2', '3', '4'], - 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'], - maxZoom: 18, - }).addTo(this.satelliteMap); + // 天地图 WMTS(Web 墨卡托,TILEMATRIXSET=w),与 Leaflet 默认坐标系一致 + 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, + } + ); + this.satelliteLayer = tdtTile('img').addTo(this.satelliteMap); + this.satelliteLabelLayer = tdtTile('cia').addTo(this.satelliteMap); } this.renderSatelliteMarkers(L); this.$nextTick(() => { if (!this.satelliteMap) return; this.satelliteMap.invalidateSize(); - this.fitSatelliteMap(); + // 如果有待定位的项目,定位到该项目;否则适应所有项目 + 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(); + } }); }) .catch(() => { @@ -868,37 +1018,40 @@ export default { }); }, getSatelliteCenter() { - if (!this.mapPoints.length) return DEFAULT_CENTER; - const lng = this.mapPoints.reduce((sum, point) => sum + Number(point.lng), 0) / this.mapPoints.length; - const lat = this.mapPoints.reduce((sum, point) => sum + Number(point.lat), 0) / this.mapPoints.length; + const points = this.selectedProject ? this.deviceMapPoints : this.mapPoints; + if (!points.length) return DEFAULT_CENTER; + 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 }; }, renderSatelliteMarkers(L) { if (!this.satelliteMap) return; this.satelliteMarkers.forEach((marker) => marker.remove()); - this.satelliteMarkers = this.mapPoints.map((project) => { - const marker = L.marker([Number(project.lat), Number(project.lng)], { + const points = this.selectedProject ? this.deviceMapPoints : this.mapPoints; + this.satelliteMarkers = points.map((point) => { + const marker = L.marker([Number(point.lat), Number(point.lng)], { icon: L.divIcon({ className: '', - html: `
${project.name || ''}
`, + html: `
${point.name || ''}
`, iconSize: [180, 34], iconAnchor: [12, 17], }), }).addTo(this.satelliteMap); - marker.on('click', () => this.selectProject(project)); + if (!this.selectedProject) marker.on('click', () => this.selectProject(point)); return marker; }); }, fitSatelliteMap() { 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); 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, { padding: [120, 120], - maxZoom: this.mapPoints.length === 1 ? 16 : 11, + maxZoom: points.length === 1 ? 16 : 11, }); }, zoomSatelliteMap(delta) { @@ -907,24 +1060,66 @@ export default { }, selectProject(project) { this.selectedProject = project; + this.fetchProjectDevices(project.id).then(() => { + this.refreshAnalysisDeviceScope(); + this.switchMode('mapView'); + this.$nextTick(() => this.renderSatelliteMap()); + }); }, - focusDevice(device) { - const project = this.projectList.find((item) => (item.devices || []).some((dev) => dev.deviceId === device.deviceId)); - if (project) { - this.selectProject(project); + isProjectSelected(project) { + return !!(this.selectedProject && String(this.selectedProject.id) === String(project.id)); + }, + 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; } - if (!Number(device.longitude) || !Number(device.latitude) || !this.mapPoints.length) return; - const nearestProject = this.mapPoints.reduce((best, current) => { - const bestDistance = best - ? Math.abs(Number(device.longitude) - Number(best.lng)) + Math.abs(Number(device.latitude) - Number(best.lat)) - : Number.POSITIVE_INFINITY; - const currentDistance = Math.abs(Number(device.longitude) - Number(current.lng)) + Math.abs(Number(device.latitude) - Number(current.lat)); - return currentDistance < bestDistance ? current : best; - }, null); - if (nearestProject) this.selectProject(nearestProject); + this.selectProject(project); + }, + refreshAnalysisDeviceScope() { + // 已选分析设备不在当前项目范围内时,清空选择与图表数据 + const selected = this.selectedAnalysisDevice; + const inScope = !selected || + !this.selectedProject || + this.projectDeviceList.some((item) => String(item.deviceId) === String(selected.deviceId)); + 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) { + this.selectedAnalysisDevice = deviceId + ? this.analysisDeviceOptions.find((item) => String(item.deviceId) === String(deviceId)) || null + : null; this.identifierList = []; this.analysisLineList = []; if (!deviceId) return; @@ -937,7 +1132,7 @@ export default { }); }, runAnalysis() { - const device = this.deviceList.find((item) => item.deviceId === this.analysisQuery.deviceId); + const device = this.selectedAnalysisDevice; if (!device) { this.$message.warning('请先选择设备'); return; @@ -1030,6 +1225,61 @@ export default { 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; + } + }, }, }; diff --git a/yack-web/src/views/bigScreen/personal-center.vue b/yack-web/src/views/bigScreen/personal-center.vue new file mode 100644 index 00000000..f51f1030 --- /dev/null +++ b/yack-web/src/views/bigScreen/personal-center.vue @@ -0,0 +1,1045 @@ + + + + + diff --git a/yack-web/src/views/iot/device/index.vue b/yack-web/src/views/iot/device/index.vue index ebc5c5a1..55f1d38c 100644 --- a/yack-web/src/views/iot/device/index.vue +++ b/yack-web/src/views/iot/device/index.vue @@ -451,6 +451,7 @@ export default { deviceName: null, productId: null, groupId: null, + projectId: null, deptId: null, productName: null, userId: null, @@ -494,6 +495,15 @@ export default { this.queryParams.groupId = 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; if (groupId != null) { @@ -534,6 +544,15 @@ export default { this.queryParams.groupId = 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; if (groupId != null) { diff --git a/yack-web/src/views/iot/project/device-select.vue b/yack-web/src/views/iot/project/device-select.vue new file mode 100644 index 00000000..ab4fae5b --- /dev/null +++ b/yack-web/src/views/iot/project/device-select.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/yack-web/src/views/iot/project/index.vue b/yack-web/src/views/iot/project/index.vue index 46d5b5f1..4aa9483d 100644 --- a/yack-web/src/views/iot/project/index.vue +++ b/yack-web/src/views/iot/project/index.vue @@ -62,9 +62,10 @@ {{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }} - +