Compare commits

...

3 Commits

  1. 209
      api/app/lib/controllers/pointDeploy/index.js
  2. 44
      api/app/lib/models/project_graph.js
  3. 53
      api/app/lib/models/project_points_deploy.js
  4. 23
      api/app/lib/routes/pointDeploy/index.js
  5. 33
      script/1.0.5/schema/2.project_graph_point_deploy.sql
  6. 72
      web/client/src/sections/projectRegime/actions/graph.js
  7. 3
      web/client/src/sections/projectRegime/actions/index.js
  8. 1
      web/client/src/sections/projectRegime/actions/projectSituation.js
  9. 57
      web/client/src/sections/projectRegime/components/pointDeploy/heatmap.js
  10. 114
      web/client/src/sections/projectRegime/components/pointDeploy/station-spot.js
  11. 91
      web/client/src/sections/projectRegime/components/pointDeploy/style.css
  12. 4
      web/client/src/sections/projectRegime/containers/index.js
  13. 9
      web/client/src/sections/projectRegime/containers/information.js
  14. 384
      web/client/src/sections/projectRegime/containers/pointDeploy/default.js
  15. 114
      web/client/src/sections/projectRegime/containers/pointDeploy/deploy-style.less
  16. 94
      web/client/src/sections/projectRegime/containers/pointDeploy/upload-img-modal.js
  17. 22
      web/client/src/sections/projectRegime/routes.js
  18. 7
      web/client/src/utils/webapi.js
  19. 2
      web/package.json

209
api/app/lib/controllers/pointDeploy/index.js

@ -0,0 +1,209 @@
'use strict';
async function findSingleGraph(ctx, next) {
let error = { name: 'FindSingleError', message: '查询单一数据失败' };
let rslt = null;
const { projectId } = ctx.params;
try {
rslt = await ctx.fs.dc.models.ProjectGraph.findOne({
where: { projectId: projectId }
});
error = null;
} catch (err) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${err}`);
}
if (error) {
ctx.status = 400;
ctx.body = error;
} else {
ctx.status = 200;
ctx.body = rslt;
}
}
async function createGraph(ctx, next) {
let error = { name: 'AddError', message: '添加数据失败' };
let graphId = null;
try {
const data = ctx.request.body;
let dataToSave = {
projectId: data.projectId,
graph: data.graph,
}
const t = await ctx.fs.dc.orm.transaction();
try {
let planarGraph = await ctx.fs.dc.models.ProjectGraph.create(dataToSave);
graphId = planarGraph.id;
await t.commit();
} catch (e) {
await t.rollback();
throw e;
}
error = null;
// 日志信息
ctx.fs.api = ctx.fs.api || {};
ctx.fs.api.actionParameter = JSON.stringify(data);
ctx.fs.api.actionParameterShow = `新增graphId:${graphId}`;
} catch (err) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${err}`);
}
if (error) {
ctx.status = 400;
ctx.body = error;
} else {
ctx.status = 200;
ctx.body = { id: graphId };
}
}
async function updateGraph(ctx, next) {
let error = { name: 'UpdateError', message: '修改数据失败' };
const { id } = ctx.params;
const data = ctx.request.body;
if (id) {
if (data && Object.keys(data).length) {
try {
const models = ctx.fs.dc.models;
let planarGraph = await models.ProjectGraph.findOne({ where: { id: id } });
const dataToSave = {};
if (planarGraph) {
const { projectId, graph } = data;
if (projectId && !(projectId == planarGraph.projectId))
dataToSave.projectId = projectId;
if (graph && !(graph == planarGraph.graph))
dataToSave.graph = graph;
}
dataToSave.id = planarGraph.id;
if (Object.keys(dataToSave).length) {
await models.ProjectGraph.update(dataToSave, { where: { id: planarGraph.id } });
}
error = null;
// 日志信息
ctx.fs.api = ctx.fs.api || {};
ctx.fs.api.actionParameter = JSON.stringify(data);
ctx.fs.api.actionParameterShow = `结构物平面图id:${data.id}`;
} catch (err) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${err}`);
}
}
} else {
error = { name: 'UpdateError', message: `不存在{id=${id}}的结构物平面图` };
}
if (error) {
ctx.status = 400;
ctx.body = error;
} else {
ctx.status = 200;
ctx.body = { message: "结构物平面图修改成功" };
}
}
async function delProjectGraph(ctx) {
try {
const { id } = ctx.params;
const models = ctx.fs.dc.models;
let info = await models.ProjectGraph.findOne({ where: { id: id } });
if (info) {
await models.ProjectPointsDeploy.destroy({ where: { graphId: id } });
await models.ProjectGraph.destroy({ where: { id } })
ctx.status = 204;
} else {
ctx.status = 400;
ctx.body = { message: '数据不存在' }
}
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
"message": "删除结构物布设图失败"
}
}
}
async function getDeployPoints(ctx) {
try {
const pictureId = ctx.params.pictureId;
const models = ctx.fs.dc.models;
const heatmap = await models.ProjectGraph.findOne({ where: { id: pictureId } })
if (heatmap) {
let allPoints = await models.Point.findAll({
attributes: ['id', 'name'],
where: { projectId: heatmap.dataValues.projectId }
})
let setedPoints = await models.ProjectPointsDeploy.findAll({
where: { graphId: pictureId }
})
ctx.status = 200;
ctx.body = {
allPoints,
setedPoints
};
} else {
throw new Error('pictureId not found');
}
} catch (err) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${err}`);
ctx.status = 400;
ctx.body = {
name: 'FindError',
message: '获取结构物平面图测点布设失败'
}
}
}
async function setDeployPoints(ctx) {
const pictureId = ctx.params.pictureId;
const req = ctx.request.body;
const models = ctx.fs.dc.models;
const orm = ctx.fs.dc.orm;
try {
ctx.fs.api = ctx.fs.api || {};
ctx.fs.api.actionParameter = req;
ctx.fs.api.actionParameterShow = '结构物平面图测点部署: ' + JSON.stringify(req);
const t = await orm.transaction();
try {
await models.ProjectPointsDeploy.destroy({ where: { graphId: pictureId }, transaction: t });
const layout = req.spots.map((hotspot, index) => {
let msg = {
graphId: pictureId,
pointId: hotspot.pointId,
position: JSON.stringify(hotspot.position)
}
return msg;
});
await models.ProjectPointsDeploy.bulkCreate(layout, { transaction: t });
await t.commit();
} catch (e) {
await t.rollback();
throw e;
}
ctx.status = 200;
ctx.body = {
name: "CreateSuccess",
message: "结构物平面图测点部署成功"
};
} catch (err) {
ctx.fs.logger.error(err);
ctx.status = 400;
ctx.body = {
name: 'CreateError',
message: "结构物平面图测点部署失败"
}
}
}
module.exports = {
findSingleGraph,
createGraph,
updateGraph,
delProjectGraph,
getDeployPoints,
setDeployPoints
};

44
api/app/lib/models/project_graph.js

@ -0,0 +1,44 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const ProjectGraph = sequelize.define("projectGraph", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: true,
unique: "project_graph_id_uindex"
},
projectId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: false,
field: "project_id",
autoIncrement: false
},
graph: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: false,
field: "graph",
autoIncrement: false
}
}, {
tableName: "project_graph",
comment: "",
indexes: []
});
dc.models.ProjectGraph = ProjectGraph;
return ProjectGraph;
};

53
api/app/lib/models/project_points_deploy.js

@ -0,0 +1,53 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const ProjectPointsDeploy = sequelize.define("projectPointsDeploy", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: true,
unique: "project_points_deploy_id_uindex"
},
pointId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: false,
field: "point_id",
autoIncrement: false
},
graphId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: false,
field: "graph_id",
autoIncrement: false
},
position: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: false,
field: "position",
autoIncrement: false
}
}, {
tableName: "project_points_deploy",
comment: "",
indexes: []
});
dc.models.ProjectPointsDeploy = ProjectPointsDeploy;
return ProjectPointsDeploy;
};

23
api/app/lib/routes/pointDeploy/index.js

@ -0,0 +1,23 @@
'use strict';
const pointDeploy = require('../../controllers/pointDeploy');
module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/project/:projectId/planarGraph'] = { content: '获取结构物平面图数据', visible: false };
router.get('/project/:projectId/planarGraph', pointDeploy.findSingleGraph);
app.fs.api.logAttr['POST/planarGraph/add'] = { content: '新增结构物平面图', visible: true };
router.post('/planarGraph/add', pointDeploy.createGraph);
app.fs.api.logAttr['PUT/planarGraph/:id/modify'] = { content: '修改结构物平面图', visible: true };
router.post('/planarGraph/:id/modify', pointDeploy.updateGraph);
app.fs.api.logAttr['DEL/project/graph/:id'] = { content: '删除结构物布设图', visible: false };
router.del('/project/graph/:id', pointDeploy.delProjectGraph);
app.fs.api.logAttr['GET/picture/:pictureId/deploy/points'] = { content: '获取点位布设信息', visible: false };
router.get('/picture/:pictureId/deploy/points', pointDeploy.getDeployPoints);
app.fs.api.logAttr['POST/set/picture/:pictureId/deploy/points'] = { content: '点位布设', visible: true };
router.post('/set/picture/:pictureId/deploy/points', pointDeploy.setDeployPoints);
};

33
script/1.0.5/schema/2.project_graph_point_deploy.sql

@ -0,0 +1,33 @@
/*结构物布设图表*/
create table project_graph
(
id serial not null,
project_id int not null,
graph varchar(255) not null
);
create unique index project_graph_id_uindex
on project_graph (id);
alter table project_graph
add constraint project_graph_pk
primary key (id);
/*点位布设表*/
create table project_points_deploy
(
id serial not null,
point_id int not null,
graph_id int not null,
position varchar(1000) not null
);
create unique index project_points_deploy_id_uindex
on project_points_deploy (id);
alter table project_points_deploy
add constraint project_points_deploy_pk
primary key (id);

72
web/client/src/sections/projectRegime/actions/graph.js

@ -0,0 +1,72 @@
'use strict';
import { basicAction } from '@peace/utils'
import { ApiTable } from '$utils'
export function getProjectGraph(projectId) {
return (dispatch) => basicAction({
type: 'get',
dispatch,
actionType: 'GET_PROJECT_PLANAR_GRAPH',
url: ApiTable.getProjectGraph.replace('{projectId}', projectId),
msg: { option: '获取结构物平面图' },
reducer: { name: 'projectGraph' }
});
}
export function createGraph(data) {
return (dispatch) => basicAction({
type: 'post',
data,
dispatch,
actionType: 'ADD_PROJECT_PLANAR_GRAPH',
url: ApiTable.createGraph,
msg: { option: '新增结构物平面图' },
});
}
export function updateGraph(id, data) {
return (dispatch) => basicAction({
type: 'post',
data,
dispatch,
actionType: 'UPDATE_PROJECT_PLANAR_GRAPH',
url: ApiTable.updateGraph.replace('{id}', id),
msg: { option: '修改结构物平面图' },
});
}
export function deleteGraph(id) {
return (dispatch) => basicAction({
type: 'del',
dispatch,
actionType: 'DELETE_PROJECT_GRAPH',
url: ApiTable.deleteGraph.replace('{id}', id),
msg: {
option: '删除结构物布设图',
},
});
}
export function getDeployPoints(pictureId) {
return (dispatch) => basicAction({
type: 'get',
dispatch,
actionType: 'GET_PROJECT_DEPLOY_POINTS',
url: ApiTable.getDeployPoints.replace('{pictureId}', pictureId),
msg: { option: '获取结构物平面图测点分布' },
reducer: { name: 'projectDeployPoints' }
});
}
export function setDeployPoints(pictureId, data) {
return (dispatch) => basicAction({
type: 'post',
data,
dispatch,
actionType: 'SET_PROJECT_DEPLOY_POINTS',
url: ApiTable.setDeployPoints.replace('{pictureId}', pictureId),
msg: { option: '结构物平面图点位布设' },
});
}

3
web/client/src/sections/projectRegime/actions/index.js

@ -2,8 +2,9 @@
import * as projectSituation from './projectSituation'
import * as projectGraph from './graph'
export default {
...projectSituation,
...projectGraph
}

1
web/client/src/sections/projectRegime/actions/projectSituation.js

@ -60,6 +60,7 @@ export function positionList (query) {
actionType: 'POSITION_LIST',
url: ApiTable.position,
msg: { error: '获取点位列表失败', },
reducer: { name: 'projectPoints' }
});
}

57
web/client/src/sections/projectRegime/components/pointDeploy/heatmap.js

@ -0,0 +1,57 @@
'use strict'
import React, { Component, PropTypes } from 'react';
import { DropTarget } from 'react-dnd';
import StationSpot from './station-spot';
const heatmapTarget = {
drop(props, monitor) {
//get item from station-spot.js
//item:{deployed, rect, spotInlist, info}
const item = monitor.getItem();
const move = monitor.getDifferenceFromInitialOffset();
props.onDeploySpot({ ...item, move });
}
};
function collect(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver()
};
}
class Heatmap extends React.Component {
componentDidMount() {
}
renderSpots() {
const { width, height, spots, onRemoveSpot } = this.props;
return spots.map(s => <StationSpot key={s.sensorId} info={s}
size={{ "width": width, "height": height }}
onRemoveSpot={onRemoveSpot} />);
}
render() {
const { connectDropTarget, height, width, image } = this.props;
let targetStyle = {
position: 'relative',
width: width,
// overflow:'hidden',
height: height,
background: `url("/_file-server/${image}") no-repeat`,
backgroundSize: '100% 100%',
};
return connectDropTarget(
<div id="dragTarget" style={targetStyle}>
{this.renderSpots()}
</div>
)
}
}
export default DropTarget('stationSpot', heatmapTarget, collect)(Heatmap);

114
web/client/src/sections/projectRegime/components/pointDeploy/station-spot.js

@ -0,0 +1,114 @@
/**
* Created by yuanfenghua on 2018/6/18.
*/
'use strict'
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import { connect } from 'react-redux';
import { DragSource } from 'react-dnd';
import { Tooltip } from 'antd';
import { MinusOutlined } from '@ant-design/icons';
import Style from './style.css';
const stationSource = {
beginDrag(props, monitor, component) {
const dom = findDOMNode(component);
const rect = {
x: dom.offsetLeft - dom.offsetParent.scrollLeft,
y: dom.offsetTop - dom.offsetParent.scrollTop
};
const spotInlist = {
x: dom.getBoundingClientRect().left,
y: dom.getBoundingClientRect().top
};
return {
info: props.info,
rect: rect,
spotInlist: spotInlist,
deployed: props.info.deployed
};
},
endDrag(props, monitor) {
if (!monitor.didDrop() && props.onRemoveSpot) {
props.onRemoveSpot(monitor.getItem().info);
}
},
canDrag(props) {
if (props.size) {
//热点图上的热点可拖拽
return true;
} else {
//测点树未布设的叶结点可拖拽
return !props.children && props.info.deployed == false
}
},
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
class StationSpot extends React.Component {
constructor(props) {
super(props);
}
renderTreeTitle = () => {
const { isDragging, info } = this.props;
const { spotId, location, deployed } = info;
const opacity = isDragging ? 0.4 : 1;
return (
<span style={{ lineHeight: '15px', opacity }} className={Style['icon']}>
<span key={spotId}>
<Tooltip title={location}>
<span className={deployed == false ? Style['station-tree-node-normal'] : null}>{location.length >= 12 ? location.substring(0, 12) + "..." : location}</span>
</Tooltip>
{deployed ? <MinusOutlined type="minus-circle-o" className={Style['tip']} onClick={this.onRemoveSpot} /> : null}
</span>
</span>
);
};
onRemoveSpot = () => {
const { onRemoveSpot, info } = this.props;
if (onRemoveSpot) {
onRemoveSpot(info);
}
};
renderHotspot = () => {
const { info, size } = this.props;
const { key, location, x, y, screenWidth, screenHeight } = info;
const { width, height } = size;
let style = {
position: 'absolute',
left: width * x / screenWidth,
top: height * y / screenHeight,
cursor: 'move'
};
return <span style={style}>
<Tooltip title={location}>
<div style={{ height: 24, width: 24, borderRadius: '100%', backgroundColor: 'rgba(16,142,233,0.2)', padding: '5px' }}>
<div style={{ height: 14, width: 14, borderRadius: '100%', backgroundColor: '#108ee9', boxShadow: '0 0 10px #108ee9' }}></div>
</div>
</Tooltip>
</span>
};
render() {
const { connectDragSource, size } = this.props;
return connectDragSource(
size ? this.renderHotspot() : this.renderTreeTitle()
);
}
}
export default connect()(DragSource('stationSpot', stationSource, collect)(StationSpot));

91
web/client/src/sections/projectRegime/components/pointDeploy/style.css

@ -0,0 +1,91 @@
.station-tree-node-normal{
margin-right: 6px;
padding: 0 4px;
height: 15px;
line-height: 15px;
background: #108ee9;
color: #fff;
font-size: 12px;
text-align: center;
}
.icon .tip{
margin-left:10px;
-webkit-transition:opacity 0.1s 0.2s;
opacity:0;
pointer-events:none;
}
.icon:hover .tip{
-webkit-transition:opacity 0.2s;
opacity:1;
pointer-events:auto;
}
.icon .tip:hover{
-webkit-transition:none;
}
:global(.no-card-border>.ant-card-head){
border: none;
}
.cardCoverBox{
position: absolute;
top: 50px;
left: 0;
right: 0;
bottom: 0;
}
.cardCover{
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-color: rgba(240 , 240, 240, 0.9);
z-index: 2;
}
.cardCover .btnCell{
text-align: center;
margin: 25px;
}
.cardFootCover{
display: flex;
justify-content: space-around;
align-items: center;
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
z-index: 1;
opacity: 0;
}
.cardFootCover:hover{
opacity: 1;
}
.deleteBox{
margin: 0 40px;
}
.deleteBox h3{
text-align: center;
margin-bottom: 5px;
}
.deleteBox .btnRow{
display: flex;
justify-content: space-around;
margin-top: 10px;
width: 100%;
}
.loadingBox{
width: 100%;
margin-bottom: 50px;
text-align: center;
padding: 50px 0;
}

4
web/client/src/sections/projectRegime/containers/index.js

@ -4,6 +4,6 @@
import QrCode from './qrCode'
import Information from './information'
import Point from './point'
import PointDeploy from './pointDeploy/default'
export { QrCode, Information, Point };
export { QrCode, Information, Point, PointDeploy };

9
web/client/src/sections/projectRegime/containers/information.js

@ -95,7 +95,7 @@ const Information = (props) => {
key: 'operation',
render: (text, record, index) => {
return (
<div style={{ width: 190 }}>
<div>
<Button type="link" onClick={() => {
setAddModel(true)
setModelData(record)
@ -123,8 +123,11 @@ const Information = (props) => {
</Popconfirm>
{/* <Button type="link" danger >二维码生成</Button> */}
<Button type="link" onClick={() => {
dispatch(push(`/projectRegime/information/${record.id}`));
dispatch(push(`/projectRegime/information/${record.id}/point`));
}} >点位</Button>
<Button type="link" onClick={() => {
dispatch(push(`/projectRegime/information/${record.id}/deploy`));
}} >布设</Button>
</div>
)
}
@ -228,7 +231,7 @@ const Information = (props) => {
)
}
function mapStateToProps (state) {
function mapStateToProps(state) {
const { auth, global } = state;
return {
user: auth.user,

384
web/client/src/sections/projectRegime/containers/pointDeploy/default.js

@ -0,0 +1,384 @@
'use strict'
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { findDOMNode } from 'react-dom';
import Heatmap from '../../components/pointDeploy/heatmap';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { Layout, Tree, Button, Input, Popconfirm, message, Spin } from 'antd'
const { Content, Sider } = Layout;
const Search = Input.Search;
const TreeNode = Tree.TreeNode;
import StationSpot from '../../components/pointDeploy/station-spot';
import './deploy-style.less';
import { getProjectGraph, deleteGraph, getDeployPoints, setDeployPoints } from '../../actions/graph';
import UploadImgModal from './upload-img-modal';
import PerfectScrollbar from 'perfect-scrollbar';
class ConfigPlanarGraph extends Component {
constructor(props) {
super(props);
this.ps = null;
this.state = {
uploadImgModal: '',
searchValue: '',
deployState: -1, //0未布,1已布,-1全部
filteredSpots: [],
spots: [],
dataHasChanged: false,
}
this.projectId = props?.match?.params?.id
}
componentDidUpdate() {
let ele = document.getElementById('security-spots-scroller');
if (ele) {
this.ps = new PerfectScrollbar(ele);
}
}
componentDidMount() {
this.getData();
}
getData = () => {
this.props.dispatch(getProjectGraph(this.projectId)).then(_ => {
if (_.success) {
let graph = _.payload.data;
if (graph) {//有图片
this.props.dispatch(getDeployPoints(graph.id));//获取平面图点位分布
} else {
this.setSpotsState([]);
}
}
});
}
componentWillReceiveProps(nextProps) {
const { projectDeployPoints } = nextProps;
if (projectDeployPoints && projectDeployPoints != this.props.projectDeployPoints) {
this.setSpotsState(projectDeployPoints);
}
}
setSpotsState = (projectDeployPoints) => {
let deployedSpotsMap = new Map();
projectDeployPoints.setedPoints?.forEach(s => {
deployedSpotsMap.set(s.pointId, s);
});
let tempData = [];
projectDeployPoints.allPoints?.map(m => {
let x = null, y = null, screenH = null, screenW = null;
let deployed = false;
let station = deployedSpotsMap.get(m.id);
if (station) {
let p = JSON.parse(station.position);
x = p.x;
y = p.y;
screenH = p.screenHeight;
screenW = p.screenWidth;
deployed = true;
}
tempData.push({
groupId: 1,
groupName: '全部',
pointId: m.id,
location: m.name,
x: x,
y: y,
screenHeight: screenH,
screenWidth: screenW,
deployed: deployed,
})
});
this.setState({
spots: tempData,
filteredSpots: tempData,
});
}
onAddImgClick = () => {
this.openPcrModal(null)
};
editHandler = (heatmap) => {
this.openPcrModal(heatmap)
};
openPcrModal = (heatmap) => {
const { dispatch } = this.props;
this.setState({
uploadImgModal: <UploadImgModal
dispatch={dispatch}
projectId={this.projectId}
pictureInfo={heatmap}
onCancel={this.closeUploadPointsImgModal}
getData={this.getData}
/>,
})
}
closeUploadPointsImgModal = () => {
this.setState({ uploadImgModal: '' })
};
onDeploySpot = (spot) => {
const { pictureInfo, clientHeight, clientWidth } = this.props;
const { spots, deployState, searchValue, partsSpots } = this.state;
const that = this;
let h = clientHeight / 1.3;
let w = clientWidth / 1.3;
function dealPosition(spot, item) {
if (spot.deployed) {
item.x = spot.rect.x + spot.move.x;
item.y = spot.rect.y + spot.move.y;
item.screenHeight = h;
item.screenWidth = w;
} else {
const boundingClientRect = findDOMNode(that.refs.heatmapComponent).getBoundingClientRect();
item.x = spot.spotInlist.x + spot.move.x - boundingClientRect.left;
item.y = spot.spotInlist.y + spot.move.y - boundingClientRect.top;
item.screenHeight = h;
item.screenWidth = w;
item.deployed = true;
}
}
if (pictureInfo) {
let tempSpots = Object.assign([], spots);
if (spot.info.pointId) {
tempSpots.forEach(item => {
if (item.pointId == spot.info.pointId) {
dealPosition(spot, item)
}
});
}
this.setState({ spots: tempSpots });
this.filterSpots(deployState, searchValue);
this.setState({ changedTreeNodeKey: spot.info.key, dataHasChanged: true });
}
};
filterSpots = (deployState, searchValue) => {
let deploySpots = this.state.spots;
if (deployState != -1) {
deploySpots = this.state.spots.filter(s => s.deployed == (deployState == 1 ? true : false));
}
let searchSpots = deploySpots;
if (searchValue.trim().length > 0) {
searchSpots = deploySpots.filter(s => s.location.indexOf(searchValue) >= 0);
}
this.setState({
searchValue,
deployState,
filteredSpots: searchSpots
});
};
onSearch = (searchValue) => {
this.filterSpots(this.state.deployState, searchValue);
};
handleStateChange = (deployState) => {
this.filterSpots(deployState, this.state.searchValue);
};
onRemoveSpot = (spot) => {
const { pictureInfo } = this.props;
const { spots, deployState, searchValue, partsSpots } = this.state;
if (pictureInfo) {
let tempSpots;
if (spot.pointId) {
tempSpots = Object.assign([], spots);
tempSpots.forEach(item => {
if (item.pointId == spot.pointId) {
item.x = null;
item.y = null;
item.screenWidth = null;
item.screenHeight = null;
item.deployed = false;
}
});
tempSpots = tempSpots.concat(partsSpots)
} else {
tempSpots = Object.assign([], partsSpots);
tempSpots.forEach(item => {
if (item.partId == spot.partId) {
item.x = null;
item.y = null;
item.screenWidth = null;
item.screenHeight = null;
item.deployed = false;
}
});
tempSpots = tempSpots.concat(spots)
}
this.filterSpots(deployState, searchValue);
this.setState({ changedTreeNodeKey: spot.key, dataHasChanged: true });
}
};
loop = (data) => {
if (!data || data.length == 0) return;
const treeNodes = [];
data.forEach((item) => {
let title = <StationSpot info={item} children={item.children} />;
if (item.children) {
treeNodes.push(
<TreeNode key={item.key} title={title}>
{this.loop(item.children)}
</TreeNode>
);
} else {
let titleProps = {
info: item,
children: item.children,
onRemoveSpot: this.onRemoveSpot,
};
//性能优化,减少组件渲染
if (this.state.changedTreeNodeKey == item.key) titleProps.key = Math.random();
let nodeTitle = <StationSpot {...titleProps} />;
treeNodes.push(<TreeNode key={item.key} title={nodeTitle} />);
}
});
return treeNodes;
};
formatTreeSource = (data) => {
if (!data || data.length == 0) return;
let tempGroups = new Map();
data.map(item => {
if (tempGroups.has(item.groupId)) {
let groupChildren = tempGroups.get(item.groupId).children;
item.key = `0-${item.groupId}-${item.pointId}`;
groupChildren.set(item.pointId, item);
} else {
tempGroups.set(item.groupId, {
'key': `0-${item.groupId}`,
'location': item.groupName,
'groupId': item.groupId,
'children': new Map(),
});
let groupChildren = tempGroups.get(item.groupId).children;
item.key = `0-${item.groupId}-${item.pointId}`;
groupChildren.set(item.pointId, item);
}
});
return tempGroups;
};
onSaveClick = () => {
this.saveHotspots(this.state.spots);
};
saveHotspots = (data) => {
const { pictureInfo } = this.props;
let postData = data.filter(s => s.x != null && s.y != null).map(item => {
const { x, y, screenWidth, screenHeight } = item;
let relativeX = parseFloat((x / screenWidth).toFixed(4));
let relativeY = parseFloat((y / screenHeight).toFixed(4));
if (item.pointId) {
return ({
pointId: item.pointId,
position: { x, y, screenWidth, screenHeight, relativeX, relativeY }
})
}
});
this.props.dispatch(setDeployPoints(pictureInfo.id, { "spots": postData })).then(res => {
message.success(res.payload.message);
this.setState({ dataHasChanged: false, deployState: -1 }, () => {
this.props.dispatch(getDeployPoints(pictureInfo.id));//获取平面图点位分布
});
});
};
render() {
const { pictureInfo, clientHeight, clientWidth } = this.props;
const { deployState, spots, filteredSpots, dataHasChanged } = this.state;
const treeDataSource = this.formatTreeSource(filteredSpots);
let h = clientHeight / 1.3;
let w = clientWidth / 1.3;
return (<div className='patrolLayout'>
<Layout>
<Sider width={230} style={{ background: 'transparent', height: 400 }}>
<div className='search-panel'>
<Button className={deployState == -1 ? 'btn-spots-filter' : 'btn-default'} style={{ borderRadius: 0 }} onClick={() => this.handleStateChange(-1)}>全部</Button>
<Button className={deployState == 1 ? 'btn-spots-filter' : 'btn-default'} style={{ borderRadius: 0 }} onClick={() => this.handleStateChange(1)}>已布</Button>
<Button className={deployState == 0 ? 'btn-spots-filter' : 'btn-default'} style={{ borderRadius: 0 }} onClick={() => this.handleStateChange(0)}>未布</Button>
<div style={{ marginTop: -1 }}>
<Search
placeholder={"请输入名称关键字搜索"}
style={{ borderRadius: 0 }}
onChange={e => this.onSearch(e.target.value)}
/>
</div>
</div>
{
treeDataSource ?
<div id='security-spots-scroller' style={{ position: 'relative', height: 600 }}>
<Tree className='equip-tree' showLine defaultExpandAll={true}>
{this.loop(treeDataSource)}
</Tree> </div> : <div style={{ textAlign: 'center' }}></div>
}
</Sider>
<Layout>
<Content className='spots-opr-content' style={{ width: w }}>
{spots && pictureInfo?.graph ?
<Heatmap key={Math.random()}
ref="heatmapComponent"
height={h}
width={w}
image={pictureInfo?.graph}
spots={spots.filter(s => s.deployed == true)}
onRemoveSpot={this.onRemoveSpot}
onDeploySpot={this.onDeploySpot}
/>
: <div style={{ border: '1px dashed #999', width: w, height: h, paddingTop: clientHeight * 0.4, textAlign: 'center' }}>
暂无热点图
</div>
}
<div className='opr-tip-row'>
<div>说明拖拽点位进行布设拖出画布移除点位</div>
{
pictureInfo ?
<div className='opr-button'>
<Popconfirm
title='确认删除该结构物布设图?(已布点位将同步删除)'
position='top'
onConfirm={() => {
this.props.dispatch(deleteGraph(pictureInfo.id)).then(_ => {
this.getData();
})
}}>
<Button className='graph-cfg-btn'>删除图片</Button>
</Popconfirm>
<Button className='graph-cfg-btn' onClick={() => this.editHandler(pictureInfo)} style={{ marginLeft: 15 }}>修改图片</Button>
<Button className='graph-cfg-btn' type='primary' onClick={this.onSaveClick} disabled={!dataHasChanged} style={{ marginLeft: 15 }}>完成</Button>
</div>
:
<div className='opr-button'>
<Button className='graph-cfg-btn' type='primary' onClick={this.onAddImgClick}>添加布设图</Button>
</div>
}
</div>
</Content>
</Layout>
</Layout>
{this.state.uploadImgModal}
</div>)
}
}
function mapStateToProps(state) {
const { global, projectGraph, projectDeployPoints } = state;
return {
pictureInfo: projectGraph.data,
projectDeployPoints: projectDeployPoints.data,
clientHeight: global.clientHeight,
clientWidth: global.clientWidth
};
}
export default connect(mapStateToProps)(DragDropContext(HTML5Backend)(ConfigPlanarGraph));

114
web/client/src/sections/projectRegime/containers/pointDeploy/deploy-style.less

@ -0,0 +1,114 @@
.search-panel {
text-align: center;
}
.search-panel input {
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
}
.spots-opr-content {
padding: 0;
margin: 0;
position: relative;
}
.opr-tip-row {
display: flex;
margin-top: 10px;
justify-content: space-between;
}
.opr-button {
.ant-btn-disabled,
.ant-btn.disabled,
.ant-btn[disabled] {
//按钮 禁用
background-color: #3198F7;
}
}
.graph-cfg-btn {
color: #fff;
background-color: #3198F7;
border-color: #3198F7;
}
//蓝色按钮 鼠标悬浮
.graph-cfg-btn:hover,
.graph-cfg-btn:focus {
//color: #fff;
background-color: #3198F7;
border-color: #3198F7;
}
.patrolLayout {
margin-top: 20px;
.ant-layout {
background-color: transparent;
}
.ant-input:hover,
.ant-input:focus {
border-color: #fff;
}
}
.equip-tree {
//测点 树结构
.ant-tree.ant-tree-show-line li span.ant-tree-switcher {
//展开, 关闭按钮
color: #fff;
}
.ant-tree li .ant-tree-node-content-wrapper {
//文字白色
color: #fff;
}
.ant-tree.ant-tree-show-line li:not(:last-child)::before {
//不显示竖线
width: 0px;
border: 0px;
}
.ant-tree li .ant-tree-node-content-wrapper.ant-tree-node-selected {
background-color: #98e5f381;
}
.ant-tree-node-content-wrapper:hover,
.ant-tree-node-content-wrapper:focus {
//文字鼠标悬浮 背景色 蓝色
background-color: #3198F7 !important;
}
}
//查询按钮
.ant-input-group-addon {
background-color: transparent;
.ant-btn {
background-color: transparent !important;
}
.ant-btn-primary {
float: right;
width: 90px;
height: 45px;
margin: 5px 0px 0px 10px;
background-color: #3198f7;
border-color: #3198f7;
}
.ant-btn-primary:hover {
border-color: #3198f7;
}
.ant-btn-primary:focus {
border-color: #3198f7;
}
}

94
web/client/src/sections/projectRegime/containers/pointDeploy/upload-img-modal.js

@ -0,0 +1,94 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import Uploads from '$components/Uploads';
import { Input, Modal, Form, Button, message, Select } from 'antd';
import { getProjectGraph, createGraph, updateGraph } from '../../actions/graph';
const DisclosureModal = (props) => {
const { dispatch, onCancel, projectId, pictureInfo, getData } = props;
let files = pictureInfo ? [{ storageUrl: pictureInfo.graph }] : []
const [form] = Form.useForm();
const [editUrl, setEditUrl] = useState(files);
//初始化表单数据
const getinitialValues = () => {
if (pictureInfo) {
return { files: 1 };
}
return {}
};
useEffect(() => {
}, []);
const handleOk = () => {
form.validateFields().then(values => {
let data = {
projectId: projectId,
graph: editUrl[0]?.storageUrl,
}
if (pictureInfo) {//更新
dispatch(updateGraph(pictureInfo.id, data)).then(_ => {
getData()
});
} else {//新增
dispatch(createGraph(data)).then(_ => {
getData();
});
}
onCancel()
})
}
const vsjunct = (params) => {
if (params.length) {
let appendix = []
for (let p of params) {
appendix.push({
fName: p.name,
size: p.size,
fileSize: p.size,
storageUrl: p.storageUrl,//必须有storageUrl
})
}
setEditUrl(appendix)
} else {
setEditUrl([])
}
}
return (
<Modal title='添加布设图' visible={true} destroyOnClose
onCancel={onCancel} onOk={handleOk}>
<Form form={form} labelCol={{ span: 6 }} wrapperCol={{ span: 17 }} initialValues={getinitialValues()}>
<Form.Item label="布设图" name='files'
rules={[{ required: true, message: '请上传布设图' }]}>
<Uploads
className='upload'
listType='card'
uploadType='project'
maxFilesNum={1}
maxFileSize={10}
isQiniu={true}
onChange={vsjunct}
fileTypes={["png", "jpeg", "jpg"]}
value={editUrl}
defaultValue={editUrl}
/>
</Form.Item>
<Form.Item style={{ paddingLeft: '17%' }}>
<div style={{ color: '#999', width: 460 }}>说明附件格式为pngjpegjpg大小不超过10MB</div>
</Form.Item>
</Form>
</Modal>
)
}
function mapStateToProps(state) {
const { auth, global } = state;
return {
user: auth.user,
actions: global.actions,
}
}
export default connect(mapStateToProps)(DisclosureModal);

22
web/client/src/sections/projectRegime/routes.js

@ -1,5 +1,5 @@
'use strict';
import { Information, QrCode, Point } from './containers';
import { Information, QrCode, Point, PointDeploy } from './containers';
export default [{
type: 'inner',
@ -13,13 +13,23 @@ export default [{
key: 'information',
breadcrumb: '结构物基础信息管理',
component: Information,
childRoutes: [ {
path: '/:id',
key: ':id',
childRoutes: [{
path: '/:id',
key: ':id',
//component: null,
breadcrumb: '结构物',
childRoutes: [{
path: '/point',
key: 'point',
component: Point,
breadcrumb: '点位',
},
]
}, {
path: '/deploy',
key: 'deploy',
component: PointDeploy,
breadcrumb: '布设',
}]
}]
}, {
path: '/qrCode',
key: 'qrCode',

7
web/client/src/utils/webapi.js

@ -130,6 +130,13 @@ export const ApiTable = {
//项目状态配置
editProjectStatus: 'project/status',
//工地平面图
getProjectGraph: 'project/{projectId}/planarGraph',
createGraph: 'planarGraph/add',
updateGraph: 'planarGraph/{id}/modify',
deleteGraph: 'project/graph/{id}',
getDeployPoints: 'picture/{pictureId}/deploy/points',
setDeployPoints: 'set/picture/{pictureId}/deploy/points',
};
export const RouteTable = {

2
web/package.json

@ -95,6 +95,8 @@
"npm": "^7.20.6",
"qrcode": "^1.5.1",
"qs": "^6.10.1",
"react-dnd": "^7",
"react-dnd-html5-backend": "^7",
"react-color": "^2.19.3",
"react-router-breadcrumbs-hoc": "^4.0.1",
"react-sortable-hoc": "^2.0.0",

Loading…
Cancel
Save