20260718:sql脚本统一

This commit is contained in:
zhugongping 2026-07-21 09:05:31 +08:00
parent 0082a69ac9
commit 47ca8800bf
30 changed files with 4975 additions and 316 deletions

View File

@ -27,7 +27,9 @@
"Bash(nvm use:*)",
"Bash(npm run:*)",
"mcp__filesystem__read_text_file",
"Bash(sips:*)"
"Bash(sips:*)",
"Bash(grep:*)",
"Bash(source ~/.nvm/nvm.sh)"
]
}
}

View File

@ -0,0 +1,790 @@
# Big Screen Transformation Implementation Plan
## Executive Summary
This plan outlines the implementation of project management functionality for the "岩安测控大数据中心" (Yan'an Control Big Data Center). The transformation includes adding business project entities, enhancing the map mode with project visualization, implementing personnel-based device viewing, and completing analysis mode integration.
## Current Architecture Understanding
### Backend Structure (RuoYi Framework)
- **Entities**: `fastbee-iot-service/src/main/java/com/fastbee/iot/domain`
- **Services**: `fastbee-iot-service/src/main/java/com/fastbee/iot/service`
- **Controllers**: `fastbee-open-api/src/main/java/com/fastbee/controller`
- **Mappers**: `fastbee-iot-service/src/main/resources/mapper/iot`
- **BaseEntity**: Extends from RuoYi framework with standard fields (createBy, createTime, updateBy, updateTime, remark)
### Frontend Structure
- **Vue Pages**: `yack-web/src/views`
- **API Files**: `yack-web/src/api`
- **BigScreen**: `yack-web/src/views/bigScreen/home.vue` (already implemented dual mode)
- **Router**: `yack-web/src/router/index.js`
### Database Schema (Existing)
- `iot_group`: Device groups (group_id, group_name, user_id)
- `iot_device_group`: Device-group relationship (device_id, group_id)
- `iot_device`: Devices with longitude, latitude, tenant_id
- `iot_device_user`: Device-user relationship (device_id, user_id)
- `sys_user`: User information
## Implementation Plan
### Phase 1: Database Schema Design
#### 1.1 Create Project Table (iot_project)
```sql
CREATE TABLE `iot_project` (
`project_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '项目ID',
`project_name` VARCHAR(100) NOT NULL COMMENT '项目名称',
`project_code` VARCHAR(50) DEFAULT NULL COMMENT '项目编码',
`tenant_id` BIGINT(20) NOT NULL COMMENT '租户ID',
`tenant_name` VARCHAR(30) DEFAULT '' COMMENT '租户名称',
`responsible_user_id` BIGINT(20) DEFAULT NULL COMMENT '负责人ID',
`responsible_user_name` VARCHAR(30) DEFAULT '' COMMENT '负责人姓名',
`longitude` DOUBLE(11,6) DEFAULT NULL COMMENT '项目经度',
`latitude` DOUBLE(11,6) DEFAULT NULL COMMENT '项目纬度',
`address` VARCHAR(255) DEFAULT NULL COMMENT '项目地址',
`project_status` TINYINT(1) DEFAULT 1 COMMENT '项目状态(1-预警,2-运行中,3-完结)',
`start_time` DATETIME DEFAULT NULL COMMENT '开始时间',
`end_time` DATETIME DEFAULT NULL COMMENT '结束时间',
`project_desc` VARCHAR(500) DEFAULT NULL COMMENT '项目描述',
`img_url` VARCHAR(255) DEFAULT NULL COMMENT '项目图片',
`del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`project_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_responsible_user` (`responsible_user_id`),
KEY `idx_project_status` (`project_status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='业务项目表';
```
#### 1.2 Create Project-Group Relationship Table (iot_project_group)
```sql
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='项目-设备分组关系表';
```
#### 1.3 Project Status Enum
Define constants for project status:
- 1 = WARNING (预警项目)
- 2 = RUNNING (运行中项目)
- 3 = COMPLETED (完结项目)
### Phase 2: Backend Implementation
#### 2.1 Entity Classes
**File**: `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Project.java`
```java
package com.fastbee.iot.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.fastbee.common.core.domain.BaseEntity;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 业务项目对象 iot_project
*/
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "Project", description = "业务项目对象 iot_project")
@Data
@TableName("iot_project")
public class Project extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty("项目ID")
private Long projectId;
@ApiModelProperty("项目名称")
private String projectName;
@ApiModelProperty("项目编码")
private String projectCode;
@ApiModelProperty("租户ID")
private Long tenantId;
@ApiModelProperty("租户名称")
private String tenantName;
@ApiModelProperty("负责人ID")
private Long responsibleUserId;
@ApiModelProperty("负责人姓名")
private String responsibleUserName;
@ApiModelProperty("项目经度")
private BigDecimal longitude;
@ApiModelProperty("项目纬度")
private BigDecimal latitude;
@ApiModelProperty("项目地址")
private String address;
@ApiModelProperty("项目状态(1-预警,2-运行中,3-完结)")
private Integer projectStatus;
@ApiModelProperty("开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@ApiModelProperty("结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
@ApiModelProperty("项目描述")
private String projectDesc;
@ApiModelProperty("项目图片")
private String imgUrl;
@ApiModelProperty("删除标志")
private String delFlag;
@ApiModelProperty("关联分组ID列表")
private List<Long> groupIds;
}
```
**File**: `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ProjectGroup.java`
```java
package com.fastbee.iot.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 项目-分组关系对象 iot_project_group
*/
@ApiModel(value = "ProjectGroup", description = "项目-设备分组关系对象 iot_project_group")
@Data
public class ProjectGroup {
@ApiModelProperty("项目ID")
private Long projectId;
@ApiModelProperty("分组ID")
private Long groupId;
}
```
#### 2.2 Service Interface and Implementation
**File**: `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IProjectService.java`
```java
package com.fastbee.iot.service;
import com.fastbee.iot.domain.Project;
import java.util.List;
/**
* 项目管理Service接口
*/
public interface IProjectService {
/**
* 查询项目
*/
public Project selectProjectById(Long projectId);
/**
* 查询项目列表
*/
public List<Project> selectProjectList(Project project);
/**
* 根据负责人查询项目
*/
public List<Project> selectProjectsByUserId(Long userId);
/**
* 根据租户查询项目
*/
public List<Project> selectProjectsByTenantId(Long tenantId);
/**
* 查询项目关联的分组ID数组
*/
public Long[] selectGroupIdsByProjectId(Long projectId);
/**
* 新增项目
*/
public int insertProject(Project project);
/**
* 修改项目
*/
public int updateProject(Project project);
/**
* 更新项目关联的分组
*/
public int updateProjectGroups(Long projectId, Long[] groupIds);
/**
* 删除项目
*/
public int deleteProjectById(Long projectId);
/**
* 批量删除项目
*/
public int deleteProjectByIds(Long[] projectIds);
/**
* 查询项目设备统计
*/
public Project selectProjectDeviceStats(Long projectId);
}
```
**File**: `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ProjectServiceImpl.java`
Implementation pattern should follow existing service implementations like GroupServiceImpl, using:
- @Autowired for mapper injection
- Transaction management for project-group relationship updates
- TenantId filtering from SecurityUtils.getLoginUser()
- delFlag = '0' for active records
#### 2.3 Mapper Interface and XML
**File**: `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/mapper/ProjectMapper.java`
```java
package com.fastbee.iot.mapper;
import com.fastbee.iot.domain.Project;
import java.util.List;
/**
* 项目Mapper接口
*/
public interface ProjectMapper {
public Project selectProjectById(Long projectId);
public List<Project> selectProjectList(Project project);
public List<Project> selectProjectsByUserId(Long userId);
public List<Project> selectProjectsByTenantId(Long tenantId);
public Long[] selectGroupIdsByProjectId(Long projectId);
public int insertProject(Project project);
public int updateProject(Project project);
public int deleteProjectById(Long projectId);
public int deleteProjectByIds(Long[] projectIds);
public int insertProjectGroups(Long projectId, Long[] groupIds);
public int deleteProjectGroupsByProjectId(Long projectId);
}
```
**File**: `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ProjectMapper.xml`
XML should follow GroupMapper.xml pattern with:
- resultMap for Project entity
- selectGroupIdsByProjectId joining iot_project_group
- insertProjectGroups batch insert
- delFlag filtering
#### 2.4 Controller Implementation
**File**: `/Users/admin/IdeaProjects/fastbee/fastbee-open-api/src/main/java/com/fastbee/controller/project/ProjectController.java`
```java
package com.fastbee.controller.project;
import com.fastbee.common.core.controller.BaseController;
import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.iot.domain.Project;
import com.fastbee.iot.service.IProjectService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "项目管理")
@RestController
@RequestMapping("/iot/project")
public class ProjectController extends BaseController {
@Autowired
private IProjectService projectService;
@ApiOperation("项目列表")
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/list")
public AjaxResult list(Project project) {
startPage();
List<Project> list = projectService.selectProjectList(project);
return getDataTable(list);
}
@ApiOperation("项目详情")
@PreAuthorize("@ss.hasPermi('iot:project:query')")
@GetMapping("/{projectId}")
public AjaxResult getInfo(@PathVariable Long projectId) {
Project project = projectService.selectProjectById(projectId);
project.setGroupIds(projectService.selectGroupIdsByProjectId(projectId));
return AjaxResult.success(project);
}
@ApiOperation("新增项目")
@PreAuthorize("@ss.hasPermi('iot:project:add')")
@PostMapping
public AjaxResult add(@RequestBody Project project) {
return toAjax(projectService.insertProject(project));
}
@ApiOperation("修改项目")
@PreAuthorize("@ss.hasPermi('iot:project:edit')")
@PutMapping
public AjaxResult edit(@RequestBody Project project) {
return toAjax(projectService.updateProject(project));
}
@ApiOperation("更新项目分组")
@PreAuthorize("@ss.hasPermi('iot:project:edit')")
@PutMapping("/updateProjectGroups")
public AjaxResult updateProjectGroups(@RequestParam Long projectId, @RequestBody Long[] groupIds) {
return toAjax(projectService.updateProjectGroups(projectId, groupIds));
}
@ApiOperation("删除项目")
@PreAuthorize("@ss.hasPermi('iot:project:remove')")
@DeleteMapping("/{projectIds}")
public AjaxResult remove(@PathVariable Long[] projectIds) {
return toAjax(projectService.deleteProjectByIds(projectIds));
}
@ApiOperation("按人员查询项目")
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/listByUser/{userId}")
public AjaxResult listByUser(@PathVariable Long userId) {
return AjaxResult.success(projectService.selectProjectsByUserId(userId));
}
@ApiOperation("项目设备统计")
@PreAuthorize("@ss.hasPermi('iot:project:query')")
@GetMapping("/deviceStats/{projectId}")
public AjaxResult deviceStats(@PathVariable Long projectId) {
return AjaxResult.success(projectService.selectProjectDeviceStats(projectId));
}
}
```
### Phase 3: Frontend Implementation
#### 3.1 Project API File
**File**: `/Users/admin/IdeaProjects/fastbee/yack-web/src/api/iot/project.js`
```javascript
import request from '@/utils/request'
// 查询项目列表
export function listProject(query) {
return request({
url: '/iot/project/list',
method: 'get',
params: query
})
}
// 查询项目详情
export function getProject(projectId) {
return request({
url: '/iot/project/' + projectId,
method: 'get'
})
}
// 新增项目
export function addProject(data) {
return request({
url: '/iot/project',
method: 'post',
data: data
})
}
// 修改项目
export function updateProject(data) {
return request({
url: '/iot/project',
method: 'put',
data: data
})
}
// 更新项目分组
export function updateProjectGroups(projectId, groupIds) {
return request({
url: '/iot/project/updateProjectGroups',
method: 'put',
params: { projectId },
data: groupIds
})
}
// 删除项目
export function delProject(projectId) {
return request({
url: '/iot/project/' + projectId,
method: 'delete'
})
}
// 按人员查询项目
export function listProjectByUser(userId) {
return request({
url: '/iot/project/listByUser/' + userId,
method: 'get'
})
}
// 项目设备统计
export function getProjectDeviceStats(projectId) {
return request({
url: '/iot/project/deviceStats/' + projectId,
method: 'get'
})
}
```
#### 3.2 Project Configuration Page
**File**: `/Users/admin/IdeaProjects/fastbee/yack-web/src/views/iot/project/index.vue`
Key features:
- Project list table with CRUD operations
- Project creation/edit form with:
- Project name, code, description
- Responsible person selection (user dropdown)
- Device group binding (multi-select)
- Map coordinate picker (integrate tdtMap.js)
- Project status selection (dropdown)
- Start/end time
- Integration with Tianditu map for coordinate selection
- Reuse patterns from `/Users/admin/IdeaProjects/fastbee/yack-web/src/views/iot/group/index.vue`
#### 3.3 Map Mode Enhancements
**File**: `/Users/admin/IdeaProjects/fastbee/yack-web/src/views/bigScreen/home.vue` (Enhancement)
Enhancements needed:
1. **Project Points Visualization**:
- Load projects via API on map initialization
- Create map markers for each project (based on longitude/latitude)
- Color markers based on project status (warning=red, running=green, completed=gray)
- Click marker to show project details panel
2. **Project Status Display**:
- Add status legend in map toolbar (already partially implemented)
- Count projects by status
- Display warningProjectCount, runningProjectCount, completedProjectCount
3. **Group Expansion**:
- Click project marker to show associated groups
- Expand groups to show devices in each group
- Device list with status indicators
4. **Code Structure**:
```javascript
// Add to data():
projectList: [],
selectedProject: null,
projectDeviceMap: {}, // projectId -> { groups: [], devices: [] }
// Add computed:
warningProjectCount() {
return this.projectList.filter(p => p.projectStatus === 1).length;
},
runningProjectCount() {
return this.projectList.filter(p => p.projectStatus === 2).length;
},
completedProjectCount() {
return this.projectList.filter(p => p.projectStatus === 3).length;
}
// Add methods:
async loadProjects() {
const res = await listProject({});
this.projectList = res.rows || [];
this.createProjectMarkers();
},
createProjectMarkers() {
// Use Tianditu API to create markers with custom icons
// Bind click events to show project details
},
async loadProjectDevices(projectId) {
// Get groupIds from project
// For each groupId, load devices
// Build projectDeviceMap
},
focusProject(project) {
// Center map on project coordinates
// Show project details panel
// Load and display associated devices
}
```
#### 3.4 View Devices by Personnel
**New Feature**: Add personnel device view panel
Implementation approach:
1. Add a panel in left column showing "人员设备视图"
2. User selection dropdown (list all users in tenant)
3. When user selected, show:
- Projects responsible by this user
- Devices in those projects (via groups)
- Device status summary
**API Endpoint** (Backend):
Add to IProjectService and implement:
```java
/**
* 查询人员负责的项目及关联设备
*/
public List<Project> selectUserProjectsAndDevices(Long userId);
```
**Frontend Integration** in home.vue:
```javascript
// Add to data:
selectedUserId: null,
userProjects: [],
userDeviceSummary: { total: 0, online: 0, offline: 0, alarms: 0 }
// Add methods:
async loadUserDeviceView(userId) {
// Load projects by responsibleUserId
// Aggregate device stats from all project groups
// Display in panel
}
```
### Phase 4: Analysis Mode Completion
#### 4.1 Data Center API Integration
The DataCenterController already provides comprehensive analysis APIs:
- `/data/center/deviceHistory` - Device historical data
- `/data/center/deviceStats` - Device statistics
- `/data/center/deviceDataQuality` - Data quality analysis
- `/data/center/deviceHistogram` - Value distribution
- `/data/center/subDeviceComparativeHistory` - Multi-device comparison
**Enhancement**: Integrate these APIs into bigScreen analysis mode
**File**: `/Users/admin/IdeaProjects/fastbee/yack-web/src/views/bigScreen/home.vue` (Analysis section)
Enhancements:
1. Connect analysisQuery device selection to project devices
2. Load identifierList from selected device's things models
3. Display analysis results using existing chart components
4. Add project-context filtering (only show devices from selected project)
### Phase 5: Menu and Permission Configuration
#### 5.1 Menu SQL Script
**File**: `/Users/admin/IdeaProjects/fastbee/sql/project-menu.sql`
```sql
-- 项目管理菜单
-- 菜单ID范围: 3600-3620
-- 父菜单: 2000 (设备管理)
INSERT INTO `sys_menu` VALUES(3600, '项目管理', 2000, 1, 'project', 'iot/project/index', NULL, 1, 0, 'C', '0', '0', 'iot:project:list', 'project', 'admin', NOW(), '', NULL, '项目管理菜单');
INSERT INTO `sys_menu` VALUES(3601, '项目查询', 3600, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:query', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO `sys_menu` VALUES(3602, '项目新增', 3600, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:add', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO `sys_menu` VALUES(3603, '项目修改', 3600, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:edit', '#', 'admin', NOW(), '', NULL, '');
INSERT INTO `sys_menu` VALUES(3604, '项目删除', 3600, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:remove', '#', 'admin', NOW(), '', NULL, '');
```
#### 5.2 Router Configuration
Add route in `/Users/admin/IdeaProjects/fastbee/yack-web/src/router/index.js`:
```javascript
{
path: '/iot/project',
component: Layout,
redirect: '/iot/project/index',
children: [
{
path: 'index',
component: () => import('@/views/iot/project/index'),
name: 'Project',
meta: { title: '项目管理', icon: 'project' }
}
]
}
```
### Phase 6: Testing and Validation
#### 6.1 Backend Testing Checklist
- Project CRUD operations with tenantId filtering
- Project-group relationship updates (batch insert/delete)
- User-project queries with proper permission checks
- Project device stats calculation (count devices in all associated groups)
- Coordinate validation (longitude/latitude ranges)
#### 6.2 Frontend Testing Checklist
- Project list display with pagination
- Project creation with coordinate picker
- Responsible user selection dropdown
- Group binding multi-select
- Map marker creation with proper icons
- Project click to show details and devices
- Personnel device view panel functionality
- Analysis mode data loading and chart rendering
### Implementation Sequence
**Week 1**: Database and Backend Core
1. Create SQL scripts for iot_project and iot_project_group tables
2. Execute SQL in database
3. Create Project.java and ProjectGroup.java entities
4. Create IProjectService interface
5. Create ProjectMapper interface and XML
6. Implement ProjectServiceImpl
7. Create ProjectController with basic CRUD APIs
8. Test backend APIs using Swagger
**Week 2**: Frontend Project Management
1. Create project.js API file
2. Create project/index.vue configuration page
3. Implement project CRUD forms
4. Integrate Tianditu coordinate picker
5. Implement group binding multi-select
6. Add menu SQL and router configuration
7. Test frontend project management page
**Week 3**: Map Mode Enhancements
1. Enhance home.vue to load projects
2. Implement project marker creation on Tianditu map
3. Add status-based marker coloring
4. Implement project click handler
5. Load project devices via group expansion
6. Display project details panel
7. Test map mode visualization
**Week 4**: Personnel View and Analysis
1. Implement backend API for user projects and devices
2. Add personnel device view panel to home.vue
3. Connect analysis mode to DataCenter APIs
4. Add project-context filtering in analysis
5. Implement device stats aggregation
6. Complete analysis chart integration
7. End-to-end testing
### Key Technical Considerations
#### Multi-Tenancy Support
- All queries must filter by tenantId from SecurityUtils.getLoginUser()
- Project tenantId should match user's tenant
- Use delFlag = '0' for active records filtering
#### Map Integration
- Reuse tdtMap.js from `/Users/admin/IdeaProjects/fastbee/yack-web/src/utils/tdtMap.js`
- Tianditu API key: c9ef88370bacf8f51662c01ad80a580c (or custom)
- Handle fallback map when Tianditu tiles fail to load
- Marker icons: Create custom SVG or use default T.Marker
#### Performance Optimization
- Lazy load project devices on marker click (not on initial load)
- Use batch queries for group-device relationships
- Implement device stats caching if needed
- Consider pagination for large device lists
#### Data Consistency
- When updating project groups: delete old relationships first, then insert new
- Validate group existence before binding
- Validate user existence as responsible person
- Check coordinate ranges (longitude: -180 to 180, latitude: -90 to 90)
### Potential Challenges
1. **Map Marker Performance**: With many projects, markers may slow down map rendering
- Solution: Implement marker clustering or lazy loading
2. **Device Aggregation**: Counting devices across multiple groups may be complex
- Solution: Use SQL aggregation queries or cache results
3. **Coordinate Accuracy**: User-entered coordinates may not be precise
- Solution: Validate coordinates and provide map picker UI
4. **Permission Complexity**: Users may need access to projects they don't own
- Solution: Implement project sharing or role-based access control
5. **Analysis Mode Scalability**: Loading large datasets for analysis may timeout
- Solution: Implement time range limits and pagination
### Dependencies
**Backend Dependencies**:
- Existing Group and Device entities
- SecurityUtils for tenant and user context
- BaseEntity for standard fields
- AjaxResult for responses
- MyBatis for data access
**Frontend Dependencies**:
- Vue.js and Element UI
- tdtMap.js for Tianditu integration
- echarts-extension-leaflet for charts
- Existing API request utility
- Vuex store for user context
**External APIs**:
- Tianditu Map API (https://api.tianditu.gov.cn/api?v=4.0)
- DataCenter APIs (existing)
### Critical Files for Implementation
The following files are most critical for implementing this plan:
1. `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/Project.java`
- Core entity defining project structure
2. `/Users/admin/IdeaProjects/fastbee/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IProjectService.java`
- Service interface defining all project operations
3. `/Users/admin/IdeaProjects/fastbee/fastbee-open-api/src/main/java/com/fastbee/controller/project/ProjectController.java`
- REST API endpoints for frontend integration
4. `/Users/admin/IdeaProjects/fastbee/yack-web/src/views/bigScreen/home.vue`
- Main big screen page requiring map mode enhancements
5. `/Users/admin/IdeaProjects/fastbee/yack-web/src/views/iot/project/index.vue`
- Project configuration management page
These files form the core of the implementation, handling data modeling, business logic, API exposure, and user interface respectively.
---
This implementation plan provides a comprehensive roadmap for transforming the big screen into a full-featured project management and visualization platform, following existing architectural patterns and ensuring seamless integration with the current system.

View File

@ -0,0 +1,199 @@
package com.fastbee.controller.project;
import com.fastbee.common.annotation.Log;
import com.fastbee.common.core.controller.BaseController;
import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.core.page.TableDataInfo;
import com.fastbee.common.enums.BusinessType;
import com.fastbee.common.utils.poi.ExcelUtil;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.domain.Project;
import com.fastbee.iot.service.IProjectService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
* 项目管理Controller
*
* @author fastbee
* @date 2026-06-28
*/
@Api(tags = "项目管理")
@RestController
@RequestMapping("/iot/project")
public class ProjectController extends BaseController {
@Autowired
private IProjectService projectService;
/**
* 查询项目列表
*/
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/list")
@ApiOperation("项目分页列表")
public TableDataInfo list(Project project) {
startPage();
List<Project> list = projectService.selectProjectList(project);
return getDataTable(list);
}
/**
* 导出项目列表
*/
@PreAuthorize("@ss.hasPermi('iot:project:export')")
@Log(title = "项目", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ApiOperation("导出项目")
public void export(HttpServletResponse response, Project project) {
List<Project> list = projectService.selectProjectList(project);
ExcelUtil<Project> util = new ExcelUtil<Project>(Project.class);
util.exportExcel(response, list, "项目数据");
}
/**
* 获取项目详细信息
*/
@PreAuthorize("@ss.hasPermi('iot:project:query')")
@GetMapping(value = "/{projectId}")
@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));
}
/**
* 新增项目
*/
@PreAuthorize("@ss.hasPermi('iot:project:add')")
@Log(title = "项目", businessType = BusinessType.INSERT)
@PostMapping
@ApiOperation("添加项目")
public AjaxResult add(@RequestBody Project project) {
return toAjax(projectService.insertProject(project));
}
/**
* 修改项目
*/
@PreAuthorize("@ss.hasPermi('iot:project:edit')")
@Log(title = "项目", businessType = BusinessType.UPDATE)
@PutMapping
@ApiOperation("修改项目")
public AjaxResult edit(@RequestBody Project project) {
return toAjax(projectService.updateProject(project));
}
/**
* 更新项目下的关联分组
*
* @param projectId 项目ID
* @param groupIds 分组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));
}
/**
* 删除项目
*/
@PreAuthorize("@ss.hasPermi('iot:project:remove')")
@Log(title = "项目", businessType = BusinessType.DELETE)
@DeleteMapping("/{projectIds}")
@ApiOperation("批量删除项目")
public AjaxResult remove(@PathVariable Long[] projectIds) {
return toAjax(projectService.deleteProjectByIds(projectIds));
}
/**
* 根据负责人ID查询项目列表
*/
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/listByUser/{userId}")
@ApiOperation("根据负责人ID查询项目列表")
public AjaxResult listByUser(@PathVariable Long userId) {
List<Project> list = projectService.selectProjectsByUserId(userId);
return AjaxResult.success(list);
}
/**
* 查询项目设备统计
*/
@PreAuthorize("@ss.hasPermi('iot:project:query')")
@GetMapping("/deviceStats/{projectId}")
@ApiOperation("查询项目设备统计")
public AjaxResult deviceStats(@PathVariable Long projectId) {
Project project = projectService.selectProjectDeviceStats(projectId);
return AjaxResult.success(project);
}
/**
* 查询用户负责的项目设备统计
*/
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/userProjectDeviceStats/{userId}")
@ApiOperation("查询用户负责的项目设备统计")
public AjaxResult userProjectDeviceStats(@PathVariable Long userId) {
Map<String, Object> stats = projectService.selectProjectDeviceStatsByUserId(userId);
return AjaxResult.success(stats);
}
/**
* 查询用户负责的项目设备列表
*/
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/userProjectDevices/{userId}")
@ApiOperation("查询用户负责的项目设备列表")
public TableDataInfo userProjectDevices(@PathVariable Long userId) {
startPage();
List<Device> list = projectService.selectProjectDevicesByUserId(userId);
return getDataTable(list);
}
/**
* 查询设备归属用户的设备统计
*/
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/userDeviceStats/{userId}")
@ApiOperation("查询设备归属用户的设备统计")
public AjaxResult userDeviceStats(@PathVariable Long userId) {
Map<String, Object> stats = projectService.selectUserDeviceStats(userId);
return AjaxResult.success(stats);
}
/**
* 查询设备归属用户的设备列表
*/
@PreAuthorize("@ss.hasPermi('iot:project:list')")
@GetMapping("/userDevices/{userId}")
@ApiOperation("查询设备归属用户的设备列表")
public TableDataInfo userDevices(@PathVariable Long userId) {
startPage();
List<Device> list = projectService.selectUserDevices(userId);
return getDataTable(list);
}
}

View File

@ -97,12 +97,12 @@ public class PubMqttCallBack implements MqttCallbackExtended {
public void connectComplete(boolean reconnect, String serverURI) {
log.info("MQTT内部客户端已经连接!");
System.out.print(
"▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌ ▌▌▌▌ ▌▌▌▌▌▍ ▌▌▌▌\n" +
" ▌▌▌▌▎▌▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌ ▌▌▌▎▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌\n" +
" ▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌▌▌▌▌▌ ▌▌▌▌\n" +
" ▌▌▌▍ ▌▌▌▍▍▍▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌▍▍▍▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌▌\n" +
" ▌▌▌▏ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌\n" +
" ▌▌▌▏ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌"
"▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌ ▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌\n" +
" ▌▌▌▌▎▌▌▌▌ ▌▌▌▎▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌ ▌▌▌▎▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌\n" +
" ▌▌▌▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌\n" +
" ▌▌▌▍ ▌▌▌▌▍▍▍▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌▍▍▍▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌\n" +
" ▌▌▌▏ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌\n" +
" ▌▌▌▏ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌"
);

View File

@ -0,0 +1,329 @@
package com.fastbee.iot.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.fastbee.common.annotation.Excel;
import com.fastbee.common.core.domain.BaseEntity;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 业务项目对象 iot_project
*
* @author fastbee
* @date 2026-06-28
*/
@ApiModel(value = "Project", description = "业务项目对象 iot_project")
public class Project extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 项目ID
*/
@ApiModelProperty("项目ID")
private Long projectId;
/**
* 项目名称
*/
@ApiModelProperty("项目名称")
@Excel(name = "项目名称")
private String projectName;
/**
* 项目编码
*/
@ApiModelProperty("项目编码")
@Excel(name = "项目编码")
private String projectCode;
/**
* 租户ID
*/
@ApiModelProperty("租户ID")
private Long tenantId;
/**
* 租户名称
*/
@ApiModelProperty("租户名称")
private String tenantName;
/**
* 负责人ID
*/
@ApiModelProperty("负责人ID")
@Excel(name = "负责人ID")
private Long responsibleUserId;
/**
* 负责人姓名
*/
@ApiModelProperty("负责人姓名")
@Excel(name = "负责人姓名")
private String responsibleUserName;
/**
* 项目经度
*/
@ApiModelProperty("项目经度")
private BigDecimal longitude;
/**
* 项目纬度
*/
@ApiModelProperty("项目纬度")
private BigDecimal latitude;
/**
* 项目地址
*/
@ApiModelProperty("项目地址")
private String address;
/**
* 项目状态(1-预警,2-运行中,3-完结)
*/
@ApiModelProperty("项目状态(1-预警,2-运行中,3-完结)")
@Excel(name = "项目状态")
private Integer projectStatus;
/**
* 开始时间
*/
@ApiModelProperty("开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "开始时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/**
* 结束时间
*/
@ApiModelProperty("结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "结束时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/**
* 项目描述
*/
@ApiModelProperty("项目描述")
private String projectDesc;
/**
* 删除标志(0代表存在 2代表删除)
*/
@ApiModelProperty("删除标志")
private String delFlag;
/**
* 关联分组ID数组(用于项目-分组关系管理)
*/
@ApiModelProperty("关联分组ID数组")
private Long[] groupIds;
/**
* 项目关联设备总数
*/
@ApiModelProperty("项目关联设备总数")
private Integer deviceCount;
/**
* 项目在线设备数
*/
@ApiModelProperty("项目在线设备数")
private Integer onlineCount;
/**
* 项目离线设备数
*/
@ApiModelProperty("项目离线设备数")
private Integer offlineCount;
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getProjectCode() {
return projectCode;
}
public void setProjectCode(String projectCode) {
this.projectCode = projectCode;
}
public Long getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
public String getTenantName() {
return tenantName;
}
public void setTenantName(String tenantName) {
this.tenantName = tenantName;
}
public Long getResponsibleUserId() {
return responsibleUserId;
}
public void setResponsibleUserId(Long responsibleUserId) {
this.responsibleUserId = responsibleUserId;
}
public String getResponsibleUserName() {
return responsibleUserName;
}
public void setResponsibleUserName(String responsibleUserName) {
this.responsibleUserName = responsibleUserName;
}
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 getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getProjectStatus() {
return projectStatus;
}
public void setProjectStatus(Integer projectStatus) {
this.projectStatus = projectStatus;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public String getProjectDesc() {
return projectDesc;
}
public void setProjectDesc(String projectDesc) {
this.projectDesc = projectDesc;
}
public String getDelFlag() {
return delFlag;
}
public void setDelFlag(String delFlag) {
this.delFlag = delFlag;
}
public Long[] getGroupIds() {
return groupIds;
}
public void setGroupIds(Long[] groupIds) {
this.groupIds = groupIds;
}
public Integer getDeviceCount() {
return deviceCount;
}
public void setDeviceCount(Integer deviceCount) {
this.deviceCount = deviceCount;
}
public Integer getOnlineCount() {
return onlineCount;
}
public void setOnlineCount(Integer onlineCount) {
this.onlineCount = onlineCount;
}
public Integer getOfflineCount() {
return offlineCount;
}
public void setOfflineCount(Integer offlineCount) {
this.offlineCount = offlineCount;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("projectId", getProjectId())
.append("projectName", getProjectName())
.append("projectCode", getProjectCode())
.append("tenantId", getTenantId())
.append("tenantName", getTenantName())
.append("responsibleUserId", getResponsibleUserId())
.append("responsibleUserName", getResponsibleUserName())
.append("longitude", getLongitude())
.append("latitude", getLatitude())
.append("address", getAddress())
.append("projectStatus", getProjectStatus())
.append("startTime", getStartTime())
.append("endTime", getEndTime())
.append("projectDesc", getProjectDesc())
.append("delFlag", getDelFlag())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("groupIds", getGroupIds())
.append("deviceCount", getDeviceCount())
.append("onlineCount", getOnlineCount())
.append("offlineCount", getOfflineCount())
.toString();
}
}

View File

@ -0,0 +1,51 @@
package com.fastbee.iot.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
/**
* 项目-分组关系对象 iot_project_group
*
* @author fastbee
* @date 2026-06-28
*/
@ApiModel(value = "ProjectGroup", description = "项目-设备分组关系对象 iot_project_group")
public class ProjectGroup {
private static final long serialVersionUID = 1L;
/**
* 项目ID
*/
@ApiModelProperty("项目ID")
private Long projectId;
/**
* 分组ID
*/
@ApiModelProperty("分组ID")
private Long groupId;
public Long getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
this.projectId = projectId;
}
public Long getGroupId() {
return groupId;
}
public void setGroupId(Long groupId) {
this.groupId = groupId;
}
@Override
public String toString() {
return "ProjectGroup{" +
"projectId=" + projectId +
", groupId=" + groupId +
'}';
}
}

View File

@ -0,0 +1,147 @@
package com.fastbee.iot.mapper;
import java.util.List;
import java.util.Map;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.domain.Project;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* 项目Mapper接口
*
* @author fastbee
* @date 2026-06-28
*/
@Repository
public interface ProjectMapper {
/**
* 查询项目
*
* @param projectId 项目主键
* @return 项目
*/
public Project selectProjectById(Long projectId);
/**
* 查询项目列表
*
* @param project 项目
* @return 项目集合
*/
public List<Project> selectProjectList(Project project);
/**
* 通过项目ID查询关联的分组ID数组
*
* @param projectId 项目ID
* @return 分组ID数组
*/
public Long[] selectGroupIdsByProjectId(Long projectId);
/**
* 根据负责人ID查询项目列表
*
* @param userId 用户ID
* @return 项目集合
*/
public List<Project> selectProjectsByUserId(Long userId);
/**
* 根据租户ID查询项目列表
*
* @param tenantId 租户ID
* @return 项目集合
*/
public List<Project> selectProjectsByTenantId(Long tenantId);
/**
* 查询用户负责的项目设备统计
*
* @param userId 用户ID
* @return 设备统计Map
*/
public Map<String, Object> selectProjectDeviceStatsByUserId(Long userId);
/**
* 查询用户负责的项目设备列表
*
* @param userId 用户ID
* @return 设备集合
*/
public List<Device> selectProjectDevicesByUserId(Long userId);
/**
* 查询设备归属用户的设备统计
*
* @param userId 用户ID
* @return 设备统计Map
*/
public Map<String, Object> selectUserDeviceStats(Long userId);
/**
* 查询设备归属用户的设备列表
*
* @param userId 用户ID
* @return 设备集合
*/
public List<Device> selectUserDevices(Long userId);
/**
* 新增项目
*
* @param project 项目
* @return 结果
*/
public int insertProject(Project project);
/**
* 修改项目
*
* @param project 项目
* @return 结果
*/
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);
/**
* 批量删除项目
*
* @param projectIds 需要删除的项目主键集合
* @return 结果
*/
public int deleteProjectByIds(Long[] projectIds);
/**
* 删除项目
*
* @param projectId 项目主键
* @return 结果
*/
public int deleteProjectById(Long projectId);
/**
* 查询项目设备统计
*
* @param projectId 项目ID
* @return 项目对象(包含统计数据)
*/
public Project selectProjectDeviceStats(Long projectId);
}

View File

@ -0,0 +1,136 @@
package com.fastbee.iot.service;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.domain.Project;
import java.util.List;
import java.util.Map;
/**
* 项目管理Service接口
*
* @author fastbee
* @date 2026-06-28
*/
public interface IProjectService {
/**
* 查询项目
*
* @param projectId 项目主键
* @return 项目
*/
public Project selectProjectById(Long projectId);
/**
* 查询项目列表
*
* @param project 项目
* @return 项目集合
*/
public List<Project> selectProjectList(Project project);
/**
* 通过项目ID查询关联的分组ID数组
*
* @param projectId 项目ID
* @return 分组ID数组
*/
public Long[] selectGroupIdsByProjectId(Long projectId);
/**
* 根据负责人ID查询项目列表
*
* @param userId 用户ID
* @return 项目集合
*/
public List<Project> selectProjectsByUserId(Long userId);
/**
* 根据租户ID查询项目列表
*
* @param tenantId 租户ID
* @return 项目集合
*/
public List<Project> selectProjectsByTenantId(Long tenantId);
/**
* 查询用户负责的项目设备统计
*
* @param userId 用户ID
* @return 设备统计Map(totalDevices, onlineDevices, offlineDevices)
*/
public Map<String, Object> selectProjectDeviceStatsByUserId(Long userId);
/**
* 查询用户负责的项目设备列表
*
* @param userId 用户ID
* @return 设备集合
*/
public List<Device> selectProjectDevicesByUserId(Long userId);
/**
* 查询设备归属用户的设备统计(现有逻辑完善)
*
* @param userId 用户ID
* @return 设备统计Map(totalDevices, onlineDevices, offlineDevices)
*/
public Map<String, Object> selectUserDeviceStats(Long userId);
/**
* 查询设备归属用户的设备列表(现有逻辑完善)
*
* @param userId 用户ID
* @return 设备集合
*/
public List<Device> selectUserDevices(Long userId);
/**
* 新增项目
*
* @param project 项目
* @return 结果
*/
public int insertProject(Project project);
/**
* 修改项目
*
* @param project 项目
* @return 结果
*/
public int updateProject(Project project);
/**
* 更新项目关联的分组
*
* @param projectId 项目ID
* @param groupIds 分组ID数组
* @return 结果
*/
public int updateProjectGroups(Long projectId, Long[] groupIds);
/**
* 批量删除项目
*
* @param projectIds 需要删除的项目主键集合
* @return 结果
*/
public int deleteProjectByIds(Long[] projectIds);
/**
* 删除项目信息
*
* @param projectId 项目主键
* @return 结果
*/
public int deleteProjectById(Long projectId);
/**
* 查询项目设备统计(包含设备总数、在线数、离线数、预警数)
*
* @param projectId 项目ID
* @return 项目对象(包含统计数据)
*/
public Project selectProjectDeviceStats(Long projectId);
}

View File

@ -1017,7 +1017,7 @@ public class DeviceServiceImpl implements IDeviceService {
.forEach(it -> it.setAlertCount(item));
}
}
fillShortListLatestReportTime(list);
fillShortListLatestReportTime(list, device.getEntrance());
return list;
}
@ -1025,8 +1025,8 @@ public class DeviceServiceImpl implements IDeviceService {
* 短时列表:从 Redis 物模型缓存聚合各点位 ts最大值写入 updateTime(方案 B)。
* 网关设备自身往往无属性上报,数据在子设备 SN 的缓存中,需合并子设备的 ts。
*/
private void fillShortListLatestReportTime(List<DeviceShortOutput> list) {
if (CollectionUtils.isEmpty(list)) {
private void fillShortListLatestReportTime(List<DeviceShortOutput> list, Integer entrance) {
if (CollectionUtils.isEmpty(list) && !Integer.valueOf(1).equals(entrance)) {
return;
}
for (DeviceShortOutput row : list) {
@ -2187,7 +2187,7 @@ public class DeviceServiceImpl implements IDeviceService {
@Override
public List<DeviceShortOutput> listTerminalUser(Device device) {
List<DeviceShortOutput> list = deviceMapper.listTerminalUser(device);
fillShortListLatestReportTime(list);
fillShortListLatestReportTime(list, device.getEntrance());
return list;
}

View File

@ -0,0 +1,264 @@
package com.fastbee.iot.service.impl;
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.ProjectMapper;
import com.fastbee.iot.service.IProjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static com.fastbee.common.utils.SecurityUtils.getLoginUser;
import static com.fastbee.common.utils.SecurityUtils.getDeptId;
/**
* 项目管理Service业务层处理
*
* @author fastbee
* @date 2026-06-28
*/
@Service
public class ProjectServiceImpl implements IProjectService {
@Autowired
private ProjectMapper projectMapper;
/**
* 查询项目
*
* @param projectId 项目主键
* @return 项目
*/
@Override
public Project selectProjectById(Long projectId) {
return projectMapper.selectProjectById(projectId);
}
/**
* 查询项目列表
*
* @param project 项目
* @return 项目集合
*/
@Override
public List<Project> selectProjectList(Project project) {
Long deptId = getDeptId();
if (Objects.isNull(deptId)) {
project.setResponsibleUserId(getLoginUser().getUserId());
} else {
Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId();
project.setTenantId(deptUserId);
}
return projectMapper.selectProjectList(project);
}
/**
* 通过项目ID查询关联的分组ID数组
*
* @param projectId 项目ID
* @return 分组ID数组
*/
@Override
public Long[] selectGroupIdsByProjectId(Long projectId) {
return projectMapper.selectGroupIdsByProjectId(projectId);
}
/**
* 根据负责人ID查询项目列表
*
* @param userId 用户ID
* @return 项目集合
*/
@Override
public List<Project> selectProjectsByUserId(Long userId) {
return projectMapper.selectProjectsByUserId(userId);
}
/**
* 根据租户ID查询项目列表
*
* @param tenantId 租户ID
* @return 项目集合
*/
@Override
public List<Project> selectProjectsByTenantId(Long tenantId) {
return projectMapper.selectProjectsByTenantId(tenantId);
}
/**
* 查询用户负责的项目设备统计
*
* @param userId 用户ID
* @return 设备统计Map(totalDevices, onlineDevices, offlineDevices)
*/
@Override
public Map<String, Object> selectProjectDeviceStatsByUserId(Long userId) {
return projectMapper.selectProjectDeviceStatsByUserId(userId);
}
/**
* 查询用户负责的项目设备列表
*
* @param userId 用户ID
* @return 设备集合
*/
@Override
public List<Device> selectProjectDevicesByUserId(Long userId) {
return projectMapper.selectProjectDevicesByUserId(userId);
}
/**
* 查询设备归属用户的设备统计(现有逻辑完善)
*
* @param userId 用户ID
* @return 设备统计Map(totalDevices, onlineDevices, offlineDevices)
*/
@Override
public Map<String, Object> selectUserDeviceStats(Long userId) {
return projectMapper.selectUserDeviceStats(userId);
}
/**
* 查询设备归属用户的设备列表(现有逻辑完善)
*
* @param userId 用户ID
* @return 设备集合
*/
@Override
public List<Device> selectUserDevices(Long userId) {
return projectMapper.selectUserDevices(userId);
}
/**
* 新增项目
* <p>
* 前端传入了 responsibleUserId 则尊重前端选择,未传则自动填充当前用户。
*
* @param project 项目
* @return 结果
*/
@Override
public int insertProject(Project project) {
Long deptId = getDeptId();
if (Objects.isNull(deptId)) {
// 终端用户:未选负责人时默认当前用户
if (project.getResponsibleUserId() == null) {
SysUser user = getLoginUser().getUser();
project.setResponsibleUserId(user.getUserId());
project.setResponsibleUserName(user.getUserName());
}
} else {
// 租户用户:未选负责人时默认当前租户管理员
if (project.getResponsibleUserId() == null) {
Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId();
String deptUserName = getLoginUser().getUser().getDept().getDeptUserName();
project.setResponsibleUserId(deptUserId);
project.setResponsibleUserName(deptUserName);
}
if (project.getTenantId() == null) {
Long deptUserId = getLoginUser().getUser().getDept().getDeptUserId();
String deptUserName = getLoginUser().getUser().getDept().getDeptUserName();
project.setTenantId(deptUserId);
project.setTenantName(deptUserName);
}
}
project.setCreateTime(DateUtils.getNowDate());
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;
}
/**
* 修改项目(含分组关联更新)
*
* @param project 项目
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
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;
}
/**
* 更新项目关联的分组(事务管理)
*
* @param projectId 项目ID
* @param groupIds 分组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);
}
return 1;
}
/**
* 批量删除项目(同时清理关联分组)
*
* @param projectIds 需要删除的项目主键集合
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteProjectByIds(Long[] projectIds) {
// 先清理关联分组关系
for (Long projectId : projectIds) {
projectMapper.deleteProjectGroupsByProjectId(projectId);
}
return projectMapper.deleteProjectByIds(projectIds);
}
/**
* 删除项目信息(同时清理关联分组)
*
* @param projectId 项目主键
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteProjectById(Long projectId) {
projectMapper.deleteProjectGroupsByProjectId(projectId);
return projectMapper.deleteProjectById(projectId);
}
/**
* 查询项目设备统计(包含设备总数、在线数、离线数、预警数)
*
* @param projectId 项目ID
* @return 项目对象(包含统计数据)
*/
@Override
public Project selectProjectDeviceStats(Long projectId) {
return projectMapper.selectProjectDeviceStats(projectId);
}
}

View File

@ -0,0 +1,259 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fastbee.iot.mapper.ProjectMapper">
<resultMap type="com.fastbee.iot.domain.Project" id="ProjectResult">
<result property="projectId" column="project_id" />
<result property="projectName" column="project_name" />
<result property="projectCode" column="project_code" />
<result property="tenantId" column="tenant_id" />
<result property="tenantName" column="tenant_name" />
<result property="responsibleUserId" column="responsible_user_id" />
<result property="responsibleUserName" column="responsible_user_name" />
<result property="longitude" column="longitude" />
<result property="latitude" column="latitude" />
<result property="address" column="address" />
<result property="projectStatus" column="project_status" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="projectDesc" column="project_desc" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="deviceCount" column="device_count" />
<result property="onlineCount" column="online_count" />
<result property="offlineCount" column="offline_count" />
</resultMap>
<resultMap type="com.fastbee.iot.domain.Device" id="DeviceResult">
<result property="deviceId" column="device_id" />
<result property="deviceName" column="device_name" />
<result property="serialNumber" column="serial_number" />
<result property="productId" column="product_id" />
<result property="productName" column="product_name" />
<result property="status" column="status" />
<result property="longitude" column="longitude" />
<result property="latitude" column="latitude" />
</resultMap>
<sql id="selectProjectVo">
select project_id, project_name, project_code, tenant_id, tenant_name,
responsible_user_id, responsible_user_name, longitude, latitude, address,
project_status, start_time, end_time, project_desc, del_flag,
create_by, create_time, update_by, update_time, remark
from iot_project
</sql>
<select id="selectProjectList" parameterType="com.fastbee.iot.domain.Project" resultMap="ProjectResult">
SELECT
p.project_id, p.project_name, p.project_code, p.tenant_id, p.tenant_name,
p.responsible_user_id, p.responsible_user_name, p.longitude, p.latitude, p.address,
p.project_status, p.start_time, p.end_time, p.project_desc, p.del_flag,
p.create_by, p.create_time, p.update_by, p.update_time, p.remark,
COUNT(DISTINCT d.device_id) AS device_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
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'
<where>
p.del_flag = '0'
<if test="projectName != null and projectName != ''"> and p.project_name like concat('%', #{projectName}, '%')</if>
<if test="projectCode != null and projectCode != ''"> and p.project_code = #{projectCode}</if>
<if test="tenantId != null"> and p.tenant_id = #{tenantId}</if>
<if test="responsibleUserId != null"> and p.responsible_user_id = #{responsibleUserId}</if>
<if test="projectStatus != null"> and p.project_status = #{projectStatus}</if>
</where>
GROUP BY p.project_id
order by p.create_time desc
</select>
<select id="selectProjectById" parameterType="Long" resultMap="ProjectResult">
<include refid="selectProjectVo"/>
where project_id = #{projectId} and del_flag = '0'
</select>
<select id="selectGroupIdsByProjectId" parameterType="Long" resultType="Long">
select group_id from iot_project_group where project_id=#{projectId}
</select>
<select id="selectProjectsByUserId" parameterType="Long" resultMap="ProjectResult">
<include refid="selectProjectVo"/>
where responsible_user_id = #{userId} and del_flag = '0'
order by create_time desc
</select>
<select id="selectProjectsByTenantId" parameterType="Long" resultMap="ProjectResult">
<include refid="selectProjectVo"/>
where tenant_id = #{tenantId} and del_flag = '0'
order by create_time desc
</select>
<!-- 查询用户负责的项目设备统计(JOIN查询) -->
<select id="selectProjectDeviceStatsByUserId" parameterType="Long" resultType="map">
SELECT
COUNT(DISTINCT d.device_id) as totalDevices,
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
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
WHERE p.responsible_user_id = #{userId}
AND p.del_flag='0'
AND d.del_flag='0'
</select>
<!-- 查询用户负责的项目设备列表(JOIN查询) -->
<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
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
WHERE p.responsible_user_id = #{userId}
AND p.del_flag='0'
AND d.del_flag='0'
ORDER BY d.device_id DESC
</select>
<!-- 查询设备归属用户的设备统计(现有逻辑) -->
<select id="selectUserDeviceStats" parameterType="Long" resultType="map">
SELECT
COUNT(d.device_id) as totalDevices,
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
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'
</select>
<!-- 查询设备归属用户的设备列表(现有逻辑) -->
<select id="selectUserDevices" parameterType="Long" resultMap="DeviceResult">
SELECT 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_user du ON d.device_id = du.device_id
WHERE du.user_id = #{userId}
AND d.del_flag='0'
ORDER BY d.device_id DESC
</select>
<insert id="insertProject" parameterType="com.fastbee.iot.domain.Project" useGeneratedKeys="true" keyProperty="projectId">
insert into iot_project
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="projectName != null and projectName != ''">project_name,</if>
<if test="projectCode != null and projectCode != ''">project_code,</if>
<if test="tenantId != null">tenant_id,</if>
<if test="tenantName != null and tenantName != ''">tenant_name,</if>
<if test="responsibleUserId != null">responsible_user_id,</if>
<if test="responsibleUserName != null and responsibleUserName != ''">responsible_user_name,</if>
<if test="longitude != null">longitude,</if>
<if test="latitude != null">latitude,</if>
<if test="address != null and address != ''">address,</if>
<if test="projectStatus != null">project_status,</if>
<if test="startTime != null">start_time,</if>
<if test="endTime != null">end_time,</if>
<if test="projectDesc != null and projectDesc != ''">project_desc,</if>
<if test="delFlag != null">del_flag,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="projectName != null and projectName != ''">#{projectName},</if>
<if test="projectCode != null and projectCode != ''">#{projectCode},</if>
<if test="tenantId != null">#{tenantId},</if>
<if test="tenantName != null and tenantName != ''">#{tenantName},</if>
<if test="responsibleUserId != null">#{responsibleUserId},</if>
<if test="responsibleUserName != null and responsibleUserName != ''">#{responsibleUserName},</if>
<if test="longitude != null">#{longitude},</if>
<if test="latitude != null">#{latitude},</if>
<if test="address != null and address != ''">#{address},</if>
<if test="projectStatus != null">#{projectStatus},</if>
<if test="startTime != null">#{startTime},</if>
<if test="endTime != null">#{endTime},</if>
<if test="projectDesc != null and projectDesc != ''">#{projectDesc},</if>
<if test="delFlag != null">#{delFlag},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</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 iot_project
<trim prefix="SET" suffixOverrides=",">
<if test="projectName != null and projectName != ''">project_name = #{projectName},</if>
<if test="projectCode != null and projectCode != ''">project_code = #{projectCode},</if>
<if test="tenantId != null">tenant_id = #{tenantId},</if>
<if test="tenantName != null and tenantName != ''">tenant_name = #{tenantName},</if>
<if test="responsibleUserId != null">responsible_user_id = #{responsibleUserId},</if>
<if test="responsibleUserName != null and responsibleUserName != ''">responsible_user_name = #{responsibleUserName},</if>
<if test="longitude != null">longitude = #{longitude},</if>
<if test="latitude != null">latitude = #{latitude},</if>
<if test="address != null and address != ''">address = #{address},</if>
<if test="projectStatus != null">project_status = #{projectStatus},</if>
<if test="startTime != null">start_time = #{startTime},</if>
<if test="endTime != null">end_time = #{endTime},</if>
<if test="projectDesc != null and projectDesc != ''">project_desc = #{projectDesc},</if>
<if test="delFlag != null">del_flag = #{delFlag},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where project_id = #{projectId}
</update>
<delete id="deleteProjectGroupsByProjectId" parameterType="Long">
delete from iot_project_group where project_id=#{projectId}
</delete>
<delete id="deleteProjectByIds" parameterType="String">
update iot_project set del_flag = '2' where project_id in
<foreach item="projectId" collection="array" open="(" separator="," close=")">
#{projectId}
</foreach>
</delete>
<delete id="deleteProjectById" parameterType="Long">
update iot_project set del_flag = '2' where project_id = #{projectId}
</delete>
<!-- 查询项目设备统计(包含设备总数、在线数、离线数、预警数) -->
<select id="selectProjectDeviceStats" parameterType="Long" resultMap="ProjectResult">
SELECT
p.*,
COUNT(DISTINCT d.device_id) as device_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
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'
WHERE p.project_id = #{projectId} AND p.del_flag = '0'
GROUP BY p.project_id
</select>
</mapper>

View File

@ -0,0 +1,67 @@
-- ========================================
-- FastBee项目管理数据库表创建脚本
-- 创建日期: 2026-06-28
-- 描述: 创建项目表和项目-分组关系表,实现User→Project→Group→Device层级
-- ========================================
-- 1. 创建项目表(iot_project)
-- 实现 User → Project 层级(通过responsible_user_id字段)
CREATE TABLE `iot_project` (
`project_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '项目ID',
`project_name` VARCHAR(100) NOT NULL COMMENT '项目名称',
`project_code` VARCHAR(50) DEFAULT NULL COMMENT '项目编码',
`tenant_id` BIGINT(20) NOT NULL COMMENT '租户ID',
`tenant_name` VARCHAR(30) DEFAULT '' COMMENT '租户名称',
`responsible_user_id` BIGINT(20) DEFAULT NULL COMMENT '负责人ID',
`responsible_user_name` VARCHAR(30) DEFAULT '' COMMENT '负责人姓名',
`longitude` DOUBLE(11,6) DEFAULT NULL COMMENT '项目经度',
`latitude` DOUBLE(11,6) DEFAULT NULL COMMENT '项目纬度',
`address` VARCHAR(255) DEFAULT NULL COMMENT '项目地址',
`project_status` TINYINT(1) DEFAULT 2 COMMENT '项目状态(1-预警,2-运行中,3-完结)',
`start_time` DATETIME DEFAULT NULL COMMENT '开始时间',
`end_time` DATETIME DEFAULT NULL COMMENT '结束时间',
`project_desc` VARCHAR(500) DEFAULT NULL COMMENT '项目描述',
`del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志(0存在 2删除)',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`project_id`),
KEY `idx_tenant_user` (`tenant_id`, `responsible_user_id`),
KEY `idx_project_status` (`project_status`),
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
--
-- 与现有逻辑兼容的关键点:
-- 1. iot_group表保持不变(现有分组管理逻辑不受影响)
-- 2. iot_device_user表保持不变(设备归属关系独立)
-- 3. 项目-分组关系是可选的(未关联项目的分组保持现有功能)
--
-- 用户查询设备的两种路径:
-- 路径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
-- WHERE p.responsible_user_id = #{userId} AND p.del_flag='0' AND d.del_flag='0'
--
-- 路径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'
-- ========================================

44
sql/project-menu.sql Normal file
View File

@ -0,0 +1,44 @@
-- ========================================
-- FastBee项目管理菜单SQL脚本
-- 创建日期: 2026-06-28
-- 描述: 插入项目管理相关菜单和权限配置到sys_menu表
-- ========================================
-- 项目管理菜单
-- 菜单ID范围: 3600-3605
-- 父菜单: 2000 (设备管理)
-- 主菜单:项目管理
INSERT INTO `sys_menu` VALUES(3600, '项目管理', 2000, 1, 'project', 'iot/project/index', NULL, 1, 0, 'C', '0', '0', 'iot:project:list', 'project', 'admin', NOW(), '', NULL, '项目管理菜单');
-- 子菜单:项目查询
INSERT INTO `sys_menu` VALUES(3601, '项目查询', 3600, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:query', '#', 'admin', NOW(), '', NULL, '');
-- 子菜单:项目新增
INSERT INTO `sys_menu` VALUES(3602, '项目新增', 3600, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:add', '#', 'admin', NOW(), '', NULL, '');
-- 子菜单:项目修改
INSERT INTO `sys_menu` VALUES(3603, '项目修改', 3600, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:edit', '#', 'admin', NOW(), '', NULL, '');
-- 子菜单:项目删除
INSERT INTO `sys_menu` VALUES(3604, '项目删除', 3600, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:remove', '#', 'admin', NOW(), '', NULL, '');
-- 子菜单:项目导出
INSERT INTO `sys_menu` VALUES(3605, '项目导出', 3600, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'iot:project:export', '#', 'admin', NOW(), '', NULL, '');
-- ========================================
-- 菜单说明:
-- menu_id: 菜单ID(3600为主菜单,3601-3605为按钮权限)
-- menu_name: 菜单名称
-- parent_id: 父菜单ID(2000为设备管理)
-- order_num: 显示顺序
-- path: 菜单路径(前端路由路径)
-- component: 组件路径(前端Vue组件路径)
-- menu_type: 菜单类型(C=目录菜单,F=按钮权限)
-- visible: 是否显示(0=显示)
-- status: 菜单状态(0=正常)
-- perms: 权限标识(iot:project:*)
-- icon: 菜单图标
-- create_by: 创建者
-- create_time: 创建时间
-- ========================================

View File

@ -0,0 +1,112 @@
import request from '@/utils/request'
// 查询项目列表
export function listProject(query) {
return request({
url: '/iot/project/list',
method: 'get',
params: query
})
}
// 查询项目详细
export function getProject(projectId) {
return request({
url: '/iot/project/' + projectId,
method: 'get'
})
}
// 查询项目下的关联分组ID数组
export function getGroupIds(projectId) {
return request({
url: '/iot/project/getGroupIds/' + projectId,
method: 'get'
})
}
// 新增项目
export function addProject(data) {
return request({
url: '/iot/project',
method: 'post',
data: data
})
}
// 修改项目
export function updateProject(data) {
return request({
url: '/iot/project',
method: 'put',
data: data
})
}
// 更新项目下的分组关联
export function updateProjectGroups(projectId, groupIds) {
return request({
url: '/iot/project/updateProjectGroups',
method: 'put',
params: { projectId },
data: groupIds
})
}
// 删除项目
export function delProject(projectId) {
return request({
url: '/iot/project/' + projectId,
method: 'delete'
})
}
// 根据负责人ID查询项目列表
export function listProjectByUser(userId) {
return request({
url: '/iot/project/listByUser/' + userId,
method: 'get'
})
}
// 查询项目设备统计
export function getProjectDeviceStats(projectId) {
return request({
url: '/iot/project/deviceStats/' + projectId,
method: 'get'
})
}
// 查询用户负责的项目设备统计
export function getUserProjectDeviceStats(userId) {
return request({
url: '/iot/project/userProjectDeviceStats/' + userId,
method: 'get'
})
}
// 查询用户负责的项目设备列表
export function getUserProjectDevices(userId, query) {
return request({
url: '/iot/project/userProjectDevices/' + userId,
method: 'get',
params: query
})
}
// 查询设备归属用户的设备统计
export function getUserDeviceStats(userId) {
return request({
url: '/iot/project/userDeviceStats/' + userId,
method: 'get'
})
}
// 查询设备归属用户的设备列表
export function getUserDevices(userId, query) {
return request({
url: '/iot/project/userDevices/' + userId,
method: 'get',
params: query
})
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783821855361" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13806" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M229.76 672c2.24 19.84 8 39.04 17.92 56.32 6.4 9.6 12.8 16 19.2 22.4 25.28 25.6 63.68 41.28 98.56 41.28 38.08 0 73.28-16 101.76-41.28l22.4-22.4 19.2-19.2c12.8-22.4 12.8-50.88-3.2-66.88-9.6-9.6-19.2-12.8-28.48-12.8-9.6 0-22.4 3.2-28.48 12.8l-41.28 41.28c-12.8 12.8-28.48 19.2-41.28 19.2s-28.48-6.4-38.08-16c-4.48-4.48-7.68-9.6-10.24-15.04a55.36 55.36 0 0 1-3.84-32.96v-0.32c0.96-5.12 2.88-10.56 5.44-15.36l0.96-1.92c2.56-4.8 5.76-9.28 9.92-13.44l0.64-0.64 9.6-9.6 63.68-63.68 28.48-28.48c9.6-9.6 19.2-16 31.68-16h9.6c9.6 0 19.2 3.2 25.6 6.4 6.4 3.2 12.8 6.4 22.4 6.4 6.4 0 16-3.2 22.4-6.4 12.8-9.6 19.2-22.4 19.2-38.08 0-6.4 0-12.8-3.2-16-3.2-9.6-9.6-16-19.2-19.2-19.2-9.6-44.48-16-66.88-16-6.4 0-16 0-22.4 3.2-25.6 3.2-50.88 16-73.28 34.88-3.2 3.2-6.4 3.2-6.4 6.4l-101.76 101.76c-16 16-25.6 31.68-31.68 47.68-1.6 2.88-1.6 5.44-2.56 7.68-3.52 10.24-5.76 20.8-6.72 31.68-0.96 10.88-0.96 21.44 0.32 32.32z" p-id="13807"></path><path d="M707.84 367.36c0 5.12-0.64 9.92-1.92 15.04v0.32c-1.28 4.48-3.2 8.64-5.44 12.8-0.64 0.96-0.96 1.92-1.6 2.88a64 64 0 0 1-8.96 11.84l-22.4 28.48-79.36 79.36c-6.4 6.4-16 12.8-22.4 12.8-6.4 3.2-12.8 3.2-19.2 3.2h-9.6c-3.2 0-6.4-3.2-9.6-3.2s-6.4-3.2-9.6-6.4c-3.2-3.2-6.4-6.4-12.8-6.4-3.2 0-9.6-3.2-12.8-3.2h-3.2c-3.2 0-9.6 0-12.8 3.2-6.4 3.2-9.6 6.4-12.8 9.6-6.4 6.4-9.6 16-9.6 25.6 0 16 6.4 28.48 19.2 38.08 16 12.8 34.88 19.2 54.08 22.4h44.48c6.4 0 16-3.2 22.4-3.2 22.4-6.4 44.48-19.2 60.48-38.08l19.2-19.2 63.68-63.68 22.4-22.4c20.8-20.8 32.32-49.92 35.2-80.64 0-1.28 0.32-2.88 0.32-4.16 0.64-10.24 0.32-20.48-1.28-30.72-4.48-28.8-17.28-56.64-40.32-78.4-6.4-6.4-16-12.8-22.4-19.2-19.2-12.8-41.28-19.2-63.68-22.4h-9.6c-28.48 0-60.48 9.6-85.76 28.48-6.4 3.2-12.8 9.6-16 16l-41.28 41.28c-16 16-16 38.08-3.2 54.08 0 3.2 3.2 3.2 3.2 6.4 9.6 9.6 19.2 12.8 28.48 12.8s22.4-3.2 28.48-12.8l7.36-7.36 19.2-19.2 15.04-15.04c12.8-12.8 28.48-19.2 41.28-19.2h9.6c9.6 3.2 19.2 6.4 28.48 12.8 6.4 6.4 10.24 13.44 12.8 21.12 1.6 4.8 2.24 9.6 2.24 14.72v1.6z" p-id="13808"></path><path d="M701.76 616c-22.72 26.24-54.4 46.08-89.6 56H960v192H64v-192h100.8c-1.6-21.12 0-42.88 4.8-64H0v320h1024V608h-314.56l-7.68 8zM0 96v320h316.8l3.2-3.2c4.16-6.08 10.24-12.16 17.92-17.28 25.28-21.12 52.8-35.84 81.28-43.52H64V160h896v192h-101.44c0.64 6.4 0.96 12.8 1.28 19.52 0.32 15.04-1.28 30.08-4.16 44.48H1024V96H0z" p-id="13809"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783821149069" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12767" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M694.613 324.779h-358.4c-18.773 0-34.133-15.36-34.133-34.134V176.128c0-18.773 15.36-34.133 34.133-34.133h358.4c18.774 0 34.134 15.36 34.134 34.133v114.517a34.015 34.015 0 0 1-34.134 34.134z m0 278.528h-358.4c-18.773 0-34.133-15.36-34.133-34.134V454.656c0-18.773 15.36-34.133 34.133-34.133h358.4c18.774 0 34.134 15.36 34.134 34.133v114.517a34.015 34.015 0 0 1-34.134 34.134z m0 278.528h-358.4c-18.773 0-34.133-15.36-34.133-34.134V733.184c0-18.773 15.36-34.133 34.133-34.133h358.4c18.774 0 34.134 15.36 34.134 34.133v114.517a34.015 34.015 0 0 1-34.134 34.134z m-459.264-69.12h-37.205c-90.453 0-164.01-73.558-164.01-164.011s73.557-164.181 164.01-164.181h37.205c18.774 0 34.134 15.36 34.134 34.133s-15.36 34.133-34.134 34.133h-37.205c-52.907 0-95.744 43.008-95.744 95.915s43.008 95.744 95.744 95.744h37.205a34.133 34.133 0 1 1 0 68.267z m590.507-273.238h-37.205c-18.774 0-34.134-15.36-34.134-34.133s15.36-34.133 34.134-34.133h37.205c52.907 0 95.915-43.008 95.915-95.915s-43.008-95.915-95.915-95.915h-37.205c-18.774 0-34.134-15.36-34.134-34.133s15.36-34.133 34.134-34.133h37.205c90.453 0 164.181 73.557 164.181 164.181S916.31 539.477 825.856 539.477z" p-id="12768"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783820468609" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9415" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M869.01 88.003H154.334C105.009 88.003 65 128.842 65 179.125v455.863c0 50.28 40.01 91.118 89.334 91.118H869.01c49.323 0 89.334-40.838 89.334-91.184V179.125c0-50.283-40.011-91.122-89.334-91.122zM585.308 789.917H438.1s-29.546 85.057-117.859 85.057v42.563h382.863v-42.563c-88.378 0-117.795-85.057-117.795-85.057zM480.43 569.319l-64.206 64.177-64.238-64.177-128.447-128.455 64.207-64.202L416.2 505.068l319.376-319.35 64.228 64.205L480.43 569.319z" p-id="9416"></path></svg>

After

Width:  |  Height:  |  Size: 802 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783820802883" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10590" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M314.49 3.082H157.692C70.793 3.082 0 74.073 0 161.271V864.32c0 87.198 70.792 158.189 157.592 158.189H314.49c86.9 0 157.592-70.991 157.592-158.189V702.652h-72.98V864.32c0 46.93-37.98 84.91-84.612 84.91H157.693c-46.731 0-84.613-38.18-84.613-84.91V161.27c0-46.929 37.981-84.91 84.613-84.91h156.896c46.73 0 84.612 38.18 84.612 84.91v159.68h72.98V161.27c-0.1-87.197-70.793-158.188-157.692-158.188z m546.252 0H703.746c-86.899 0-157.592 70.991-157.592 158.189v159.68h72.98V161.27c0-46.93 37.981-84.911 84.612-84.911h156.896c46.731 0 84.613 38.18 84.613 84.91v703.05c0 46.93-37.981 84.91-84.613 84.91H703.746c-46.73 0-84.612-38.18-84.612-84.91V702.652h-72.98V864.32c0 87.198 70.792 158.189 157.592 158.189h156.896c86.9 0 157.592-70.991 157.592-158.189V161.27c0.1-87.197-70.692-158.188-157.492-158.188z m0 0" p-id="10591"></path><path d="M191.995 485.602H461.84c-0.596-27.74-23.465-50.31-51.304-50.31H186.129c-28.238 0-51.305 23.166-51.305 51.503v105.492c0 28.337 23.067 51.504 51.305 51.504h224.506c28.237 0 51.305-23.167 51.305-51.504v-1.093H191.995V485.602z m0 0" p-id="10592"></path><path d="M395.423 512.447h230.074v46.035H395.423z" p-id="10593"></path><path d="M556.395 485.602H826.34v105.492H556.395v1.094c0 28.337 23.067 51.503 51.305 51.503h224.506c28.238 0 51.305-23.166 51.305-51.503V486.696c0-28.337-23.067-51.503-51.305-51.503H607.7c-27.84 0.099-50.708 22.669-51.305 50.41z m0 0" p-id="10594"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783820084946" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5151" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M987.456 425.152H864V295.296a36.48 36.48 0 0 0-36.544-36.544H467.456L333.376 130.496A9.344 9.344 0 0 0 327.04 128H36.48A36.48 36.48 0 0 0 0 164.544v676.608a36.48 36.48 0 0 0 36.544 36.544h797.76a36.672 36.672 0 0 0 33.92-22.848L1021.44 475.52a36.48 36.48 0 0 0-33.92-50.304zM82.304 210.304h215.424l136.64 130.752h347.328v84.096H198.848a36.672 36.672 0 0 0-33.92 22.848l-82.624 204.8V210.304z m726.016 585.152H108.544l118.08-292.608h699.904L808.32 795.52z" fill="#777777" p-id="5152"></path></svg>

After

Width:  |  Height:  |  Size: 830 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783820110142" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5297" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M182.857143 164.571429h256a18.285714 18.285714 0 0 1 18.285714 18.285714v256a18.285714 18.285714 0 0 1-18.285714 18.285714H182.857143a18.285714 18.285714 0 0 1-18.285714-18.285714V182.857143a18.285714 18.285714 0 0 1 18.285714-18.285714z m336.347428 133.339428l181.028572-181.028571a18.285714 18.285714 0 0 1 25.856 0l181.028571 181.028571a18.285714 18.285714 0 0 1 0 25.892572l-181.028571 181.028571a18.285714 18.285714 0 0 1-25.892572 0l-181.028571-181.028571a18.285714 18.285714 0 0 1 0-25.892572zM182.857143 566.857143h256a18.285714 18.285714 0 0 1 18.285714 18.285714v256a18.285714 18.285714 0 0 1-18.285714 18.285714H182.857143a18.285714 18.285714 0 0 1-18.285714-18.285714v-256a18.285714 18.285714 0 0 1 18.285714-18.285714z m402.285714 0h256a18.285714 18.285714 0 0 1 18.285714 18.285714v256a18.285714 18.285714 0 0 1-18.285714 18.285714h-256a18.285714 18.285714 0 0 1-18.285714-18.285714v-256a18.285714 18.285714 0 0 1 18.285714-18.285714zM201.142857 201.142857v219.428572h219.428572v-219.428572h-219.428572z m356.827429 109.714286l155.172571 155.172571L868.315429 310.857143 713.142857 155.684571 557.970286 310.857143z m-356.827429 292.571428v219.428572h219.428572v-219.428572h-219.428572z m402.285714 0v219.428572h219.428572v-219.428572h-219.428572z" fill="#358EFF" p-id="5298"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1783822390125" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14823" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M64 94.779v834.443L94.779 960h834.443L960 929.221V94.779L929.221 64H94.779L64 94.779z m64.977 30.778h769.466v769.466H128.977V125.557z m61.557 707.909h129.954V190.534H190.534v642.932z m386.443-256.489H447.023V190.534h129.954v386.443z m126.534 126.534h129.954V190.534H703.511v512.977z" p-id="14824"></path></svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@ -17,7 +17,7 @@ router.beforeEach((to, from, next) => {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title);
/* has token*/
if (to.path === '/login') {
next({ path: '/' });
next({ path: '/bigscreen' });
NProgress.done();
} else {
if (store.getters.roles.length === 0) {
@ -30,7 +30,7 @@ router.beforeEach((to, from, next) => {
store.dispatch('GenerateRoutes').then((accessRoutes) => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes); // 动态添加可访问路由表
next({ ...to, replace: true }); // hack方法 确保addRoutes已完成
next({ path: '/bigscreen', replace: true }); // 登录后跳转到大数据看板
});
})
.catch((err) => {

View File

@ -42,7 +42,8 @@ export const constantRoutes = [
],
},
{
path: '/bigScreen',
path: '/bigscreen',
alias: ['/bigScreen', '/largeScreen', '/mapView', '/analysis'],
component: () => import('@/views/bigScreen/home'),
hidden: true,
meta: { bigScreen: true },
@ -70,7 +71,7 @@ export const constantRoutes = [
{
path: '',
component: Layout,
redirect: 'index',
redirect: '/bigscreen',
children: [
{
path: 'index',

View File

@ -0,0 +1,41 @@
const DEFAULT_TIANDITU_AK = process.env.VUE_APP_TIANDITU_AK || 'c9ef88370bacf8f51662c01ad80a580c';
const DEFAULT_TIANDITU_TILE_KEY = process.env.VUE_APP_TIANDITU_TILE_KEY || 'b920e6076a06aa279c974f5f76cf746e';
let tiandituLoading = null;
export function loadTiandituMap(tk = DEFAULT_TIANDITU_AK) {
if (window.T) {
return Promise.resolve(window.T);
}
if (tiandituLoading) {
return tiandituLoading;
}
tiandituLoading = new Promise((resolve, reject) => {
const existingScript = document.getElementById('tianditu-js-api');
if (existingScript) {
existingScript.addEventListener('load', () => resolve(window.T));
existingScript.addEventListener('error', reject);
return;
}
const script = document.createElement('script');
script.id = 'tianditu-js-api';
script.type = 'text/javascript';
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) {
resolve(window.T);
} else {
reject(new Error('天地图脚本已加载,但未找到 T 对象'));
}
};
script.onerror = reject;
document.head.appendChild(script);
});
return tiandituLoading;
}
export { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -500,6 +500,7 @@ export default {
this.queryParams.groupId = Number(groupId);
this.queryParams.productId = null;
this.queryParams.serialNumber = null;
this.searchShow = true;
}
// 设备编号筛选
let sn = this.$route.query.sn;
@ -539,6 +540,7 @@ export default {
this.queryParams.groupId = Number(groupId);
this.queryParams.productId = null;
this.queryParams.serialNumber = null;
this.searchShow = true;
}
// 设备编号筛选
let sn = this.$route.query.sn;

View File

@ -0,0 +1,596 @@
<template>
<div class="iot-project">
<el-card v-show="showSearch" style="margin-bottom: 10px">
<el-form ref="queryForm" :model="queryParams" :inline="true" size="mini" label-width="68px">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="项目状态" prop="projectStatus">
<el-select v-model="queryParams.projectStatus" placeholder="请选择项目状态" clearable style="width: 120px">
<el-option label="预警" :value="1" />
<el-option label="运行中" :value="2" />
<el-option label="完结" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="负责人" prop="responsibleUserId">
<el-select v-model="queryParams.responsibleUserId" placeholder="请选择负责人" clearable filterable style="width: 150px">
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
<el-form-item style="float: right">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<el-row :gutter="10" style="margin-bottom: 15px">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['iot:project:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['iot:project:remove']">删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="项目名称" align="center" prop="projectName" min-width="150" />
<el-table-column label="项目编码" align="center" prop="projectCode" width="120" />
<el-table-column label="负责人" align="center" width="100">
<template slot-scope="scope">
<span>{{ getUserName(scope.row.responsibleUserId, scope.row.responsibleUserName) }}</span>
</template>
</el-table-column>
<el-table-column label="项目状态" align="center" prop="projectStatus" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.projectStatus)">{{ getStatusText(scope.row.projectStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="坐标" align="center" width="150">
<template slot-scope="scope">
<span v-if="scope.row.longitude && scope.row.latitude">
{{ scope.row.longitude }}°, {{ scope.row.latitude }}°
</span>
<span v-else style="color: #999">未设置</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="260">
<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-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-delete" @click="handleDelete(scope.row)" v-hasPermi="['iot:project:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改项目对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编码" prop="projectCode">
<el-input v-model="form.projectCode" placeholder="请输入项目编码" :disabled="isView" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="负责人" prop="responsibleUserId">
<el-select v-model="form.responsibleUserId" placeholder="请选择负责人" filterable style="width: 100%" :disabled="isView" @change="handleResponsibleUserChange">
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目状态" prop="projectStatus">
<el-select v-model="form.projectStatus" placeholder="请选择项目状态" style="width: 100%" :disabled="isView">
<el-option label="预警" :value="1" />
<el-option label="运行中" :value="2" />
<el-option label="完结" :value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" :disabled="isView" />
</el-form-item>
</el-col>
</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-row :gutter="10">
<el-col :span="8">
<el-input v-model.number="form.longitude" placeholder="经度" type="number" :disabled="isView" />
</el-col>
<el-col :span="8">
<el-input v-model.number="form.latitude" placeholder="纬度" type="number" :disabled="isView" />
</el-col>
<el-col :span="8">
<el-button type="primary" size="small" @click="openMapPicker" :disabled="isView">地图选点</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="关联分组" prop="groupIds">
<el-select v-model="form.groupIds" multiple placeholder="请选择设备分组" style="width: 100%" :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 label="项目描述" prop="projectDesc">
<el-input v-model="form.projectDesc" type="textarea" placeholder="请输入项目描述" :rows="3" :disabled="isView" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<template v-if="isView">
<el-button @click="open = false">关 闭</el-button>
</template>
<template v-else>
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</template>
</div>
</el-dialog>
<!-- 地图坐标选择对话框 -->
<el-dialog title="地图选点" :visible.sync="mapPickerOpen" width="800px" append-to-body @closed="disposeMap">
<div style="height: 400px; border: 1px solid #ddd; position: relative">
<div ref="mapContainer" style="width: 100%; height: 100%"></div>
<div v-if="geocoding" style="position: absolute; top: 8px; right: 8px; background: rgba(255,255,255,0.9); padding: 4px 10px; border-radius: 3px; font-size: 12px; z-index: 999">
<i class="el-icon-loading"></i> 地址解析中...
</div>
</div>
<div style="margin-top: 10px; display: flex; justify-content: space-between; align-items: center">
<span>经度: {{ pickerLongitude }}°, 纬度: {{ pickerLatitude }}°</span>
<span v-if="pickerAddress" style="color: #606266; font-size: 13px">{{ pickerAddress }}</span>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmCoordinates">确 定</el-button>
<el-button @click="mapPickerOpen = false">取 消</el-button>
</div>
</el-dialog>
<!-- 多分组查看设备选择 -->
<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>
</template>
<script>
import { listProject, getProject, delProject, addProject, updateProject } from '@/api/iot/project';
import { listGroup } from '@/api/iot/group';
import { listUser } from '@/api/system/user';
import { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY, loadTiandituMap } from '@/utils/tdtMap';
export default {
name: 'Project',
data() {
return {
loading: true,
ids: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
projectList: [],
userList: [],
groupList: [],
title: '',
open: false,
isView: false,
mapPickerOpen: false,
groupSelectOpen: false,
pickerLongitude: null,
pickerLatitude: null,
pickerAddress: '',
geocoding: false,
selectableGroupList: [],
selectedGroupId: null,
queryParams: {
pageNum: 1,
pageSize: 10,
projectName: null,
projectStatus: null,
responsibleUserId: null,
},
form: {},
rules: {
projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
responsibleUserId: [{ required: true, message: '请选择负责人', trigger: 'change' }],
projectStatus: [{ required: true, message: '请选择项目状态', trigger: 'change' }],
},
// 天地图实例
tiandituMap: null,
tiandituMarker: null,
tiandituGeocoder: null,
};
},
created() {
this.getList();
this.getUserList();
this.getGroupList();
},
beforeDestroy() {
this.disposeMap();
},
methods: {
/** 查询项目列表 */
getList() {
this.loading = true;
listProject(this.queryParams).then(response => {
this.projectList = response.rows;
this.total = response.total;
this.loading = false;
}).catch(() => {
this.loading = false;
});
},
/** 查询用户列表 */
getUserList() {
listUser({ pageNum: 1, pageSize: 1000 }).then(response => {
this.userList = response.rows || [];
});
},
/** 查询分组列表 */
getGroupList() {
listGroup({ pageNum: 1, pageSize: 1000 }).then(response => {
this.groupList = response.rows || [];
});
},
/** 获取负责人名称 */
getUserName(userId, userName) {
if (userName) return userName;
const user = this.userList.find(item => Number(item.userId) === Number(userId));
return user ? user.nickName : '';
},
/** 负责人变更 */
handleResponsibleUserChange(userId) {
const user = this.userList.find(item => Number(item.userId) === Number(userId));
this.form.responsibleUserName = user ? user.nickName : null;
},
/** 规范化项目表单 */
normalizeProjectForm(data) {
if (!data) return data;
if (data.responsibleUserId != null) {
data.responsibleUserId = Number(data.responsibleUserId);
}
if (data.projectStatus != null) {
data.projectStatus = Number(data.projectStatus);
}
if (!data.responsibleUserName && data.responsibleUserId != null) {
data.responsibleUserName = this.getUserName(data.responsibleUserId, data.responsibleUserName);
}
return data;
},
/** 状态文本 */
getStatusText(status) {
const map = { 1: '预警', 2: '运行中', 3: '完结' };
return map[status] || '未知';
},
/** 状态标签颜色 */
getStatusTagType(status) {
const map = { 1: 'danger', 2: 'success', 3: 'info' };
return map[status] || '';
},
/** 取消 */
cancel() {
this.open = false;
this.isView = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
projectId: null,
projectName: null,
projectCode: null,
responsibleUserId: null,
responsibleUserName: null,
longitude: null,
latitude: null,
address: null,
projectStatus: 2,
startTime: null,
endTime: null,
projectDesc: null,
groupIds: [],
};
this.isView = false;
this.resetForm('form');
},
/** 搜索 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置搜索 */
resetQuery() {
this.resetForm('queryForm');
this.handleQuery();
},
/** 多选 */
handleSelectionChange(selection) {
this.ids = selection.map(item => item.projectId);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
/** 新增 */
handleAdd() {
this.reset();
this.open = true;
this.title = '添加项目';
},
/** 查看(只读) */
handleView(row) {
this.reset();
this.isView = true;
this.open = true;
this.title = '查看项目';
const projectId = row.projectId || this.ids[0];
getProject(projectId).then(response => {
this.form = this.normalizeProjectForm(response.data);
});
},
/** 查看项目设备 */
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({
path: '/iot/device',
query: {
groupId,
t: Date.now(),
},
});
},
/** 确认查看所选分组设备 */
confirmViewDevices() {
if (!this.selectedGroupId) {
this.$message.warning('请选择设备分组');
return;
}
this.groupSelectOpen = false;
this.goDeviceByGroup(this.selectedGroupId);
},
/** 编辑 */
handleUpdate(row) {
this.reset();
this.open = true;
this.title = '修改项目';
this.$nextTick(() => {
const projectId = row.projectId || this.ids[0];
getProject(projectId).then(response => {
this.form = this.normalizeProjectForm(response.data);
});
});
},
/** 提交 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
this.handleResponsibleUserChange(this.form.responsibleUserId);
if (this.form.projectId != null) {
updateProject(this.form).then(() => {
this.$modal.msgSuccess('修改成功');
this.open = false;
this.getList();
});
} else {
addProject(this.form).then(() => {
this.$modal.msgSuccess('新增成功');
this.open = false;
this.getList();
});
}
}
});
},
/** 删除 */
handleDelete(row) {
const projectIds = row.projectId ? [row.projectId] : this.ids;
const names = row.projectName || '所选项目';
this.$modal.confirm('是否确认删除"' + names + '"?').then(() => {
return delProject(projectIds);
}).then(() => {
this.getList();
this.$modal.msgSuccess('删除成功');
}).catch(() => {});
},
// ==================== 地图选点 ====================
/** 打开地图选点 */
openMapPicker() {
this.pickerLongitude = this.form.longitude || null;
this.pickerLatitude = this.form.latitude || null;
this.pickerAddress = this.form.address || '';
this.mapPickerOpen = true;
this.$nextTick(() => {
this.initTiandituMap();
});
},
/** 初始化天地图 */
initTiandituMap() {
loadTiandituMap().then(T => {
const container = this.$refs.mapContainer;
if (!container) return;
const centerLng = this.pickerLongitude || 104.0;
const centerLat = this.pickerLatitude || 35.0;
const zoom = (this.pickerLongitude && this.pickerLatitude) ? 12 : 4;
if (this.tiandituMap) {
this.tiandituMap.destroy();
this.tiandituMap = null;
}
const map = new T.Map(container);
map.centerAndZoom(new T.LngLat(centerLng, centerLat), zoom);
map.addControl(new T.Control.Zoom());
this.tiandituGeocoder = T.Geocoder ? new T.Geocoder() : null;
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
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.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' }));
// 已有坐标则添加标记
if (this.pickerLongitude && this.pickerLatitude) {
const point = new T.LngLat(this.pickerLongitude, this.pickerLatitude);
this.tiandituMarker = new T.Marker(point);
map.addOverLay(this.tiandituMarker);
}
// 点击选点
map.addEventListener('click', (e) => {
const lnglat = e.lnglat;
this.pickerLongitude = Number(lnglat.getLng().toFixed(6));
this.pickerLatitude = Number(lnglat.getLat().toFixed(6));
// 同步更新表单坐标字段
this.form.longitude = this.pickerLongitude;
this.form.latitude = this.pickerLatitude;
if (this.tiandituMarker) {
map.removeOverLay(this.tiandituMarker);
}
this.tiandituMarker = new T.Marker(new T.LngLat(this.pickerLongitude, this.pickerLatitude));
map.addOverLay(this.tiandituMarker);
this.reverseGeocode(this.pickerLongitude, this.pickerLatitude);
});
this.tiandituMap = map;
}).catch(() => {
this.$message.error('天地图加载失败,请检查网络');
});
},
/** 逆地理编码:经纬度 → 地址 */
reverseGeocode(lng, lat) {
this.geocoding = true;
if (this.tiandituGeocoder && window.T) {
this.tiandituGeocoder.getLocation(new window.T.LngLat(lng, lat), result => {
this.geocoding = false;
this.applyGeocodeAddress(result);
});
return;
}
const cbName = '_tianditu_geocoder_' + Date.now();
window[cbName] = (response) => {
this.geocoding = false;
this.applyGeocodeAddress(response);
delete window[cbName];
const script = document.getElementById(cbName + '_script');
if (script) script.remove();
};
const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
const postStr = encodeURIComponent(JSON.stringify({ lon: lng, lat: lat, ver: 1 }));
const script = document.createElement('script');
script.id = cbName + '_script';
script.src = `${protocol}//api.tianditu.gov.cn/geocoder?postStr=${postStr}&type=geocode&tk=${DEFAULT_TIANDITU_AK}&callback=${cbName}`;
script.onerror = () => {
this.geocoding = false;
delete window[cbName];
};
document.head.appendChild(script);
},
/** 设置逆地理地址 */
applyGeocodeAddress(response) {
try {
const result = response && response.result ? response.result : response;
const data = typeof result === 'string' ? JSON.parse(result) : result;
const component = data && (data.addressComponent || data.address_component);
const addr = (data && (data.formatted_address || data.formattedAddress || data.address))
|| (component ? [component.province, component.city, component.county, component.road, component.poi].filter(Boolean).join('') : '');
if (addr) {
this.pickerAddress = addr;
this.form.address = addr;
}
} catch (e) {
this.pickerAddress = '';
}
},
/** 确认坐标 */
confirmCoordinates() {
this.form.longitude = this.pickerLongitude;
this.form.latitude = this.pickerLatitude;
if (this.pickerAddress) {
this.form.address = this.pickerAddress;
}
this.mapPickerOpen = false;
},
/** 销毁地图实例 */
disposeMap() {
if (this.tiandituMarker && this.tiandituMap) {
this.tiandituMap.removeOverLay(this.tiandituMarker);
this.tiandituMarker = null;
}
if (this.tiandituMap) {
this.tiandituMap.destroy();
this.tiandituMap = null;
}
},
},
};
</script>
<style scoped>
.iot-project {
padding: 20px;
}
</style>

View File

@ -362,7 +362,7 @@ export default {
setToken(res.token);
this.$router
.push({
path: this.redirect || '/',
path: '/bigscreen',
})
.catch(() => {});
if (this.captchaOnOff) {
@ -484,7 +484,7 @@ export default {
setToken(res.data);
this.$router
.push({
path: '/',
path: '/bigscreen',
})
.catch(() => {});
});
@ -517,7 +517,7 @@ export default {
setToken(res.token);
this.$router
.push({
path: '/',
path: '/bigscreen',
})
.catch(() => {});
})
@ -572,7 +572,7 @@ export default {
.then(() => {
this.$router
.push({
path: this.redirect || '/',
path: '/bigscreen',
})
.catch(() => {});
})