diff --git a/web/client/src/sections/projectRegime/actions/graph.js b/web/client/src/sections/projectRegime/actions/graph.js
new file mode 100644
index 0000000..6a8e2f5
--- /dev/null
+++ b/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: '结构物平面图点位布设' },
+ });
+}
\ No newline at end of file
diff --git a/web/client/src/sections/projectRegime/actions/index.js b/web/client/src/sections/projectRegime/actions/index.js
index 4e50055..8e9388b 100644
--- a/web/client/src/sections/projectRegime/actions/index.js
+++ b/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
}
\ No newline at end of file
diff --git a/web/client/src/sections/projectRegime/actions/projectSituation.js b/web/client/src/sections/projectRegime/actions/projectSituation.js
index ab045b8..f159551 100644
--- a/web/client/src/sections/projectRegime/actions/projectSituation.js
+++ b/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' }
});
}
diff --git a/web/client/src/sections/projectRegime/components/pointDeploy/heatmap.js b/web/client/src/sections/projectRegime/components/pointDeploy/heatmap.js
new file mode 100644
index 0000000..33545ac
--- /dev/null
+++ b/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 => );
+ }
+
+ 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(
+
+
)
}
@@ -228,7 +231,7 @@ const Information = (props) => {
)
}
-function mapStateToProps (state) {
+function mapStateToProps(state) {
const { auth, global } = state;
return {
user: auth.user,
diff --git a/web/client/src/sections/projectRegime/containers/pointDeploy/default.js b/web/client/src/sections/projectRegime/containers/pointDeploy/default.js
new file mode 100644
index 0000000..fa3f90f
--- /dev/null
+++ b/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:
,
+ })
+ }
+
+ 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 =
;
+ if (item.children) {
+ treeNodes.push(
+
+ {this.loop(item.children)}
+
+ );
+ } else {
+ let titleProps = {
+ info: item,
+ children: item.children,
+ onRemoveSpot: this.onRemoveSpot,
+ };
+ //性能优化,减少组件渲染
+ if (this.state.changedTreeNodeKey == item.key) titleProps.key = Math.random();
+ let nodeTitle =
;
+ treeNodes.push(
);
+ }
+ });
+ 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 (
+
+
+
+
this.handleStateChange(-1)}>全部
+
this.handleStateChange(1)}>已布
+
this.handleStateChange(0)}>未布
+
+ this.onSearch(e.target.value)}
+ />
+
+
+ {
+ treeDataSource ?
+
+
+ {this.loop(treeDataSource)}
+
: 暂无点位
+ }
+
+
+
+ {spots && pictureInfo?.graph ?
+ s.deployed == true)}
+ onRemoveSpot={this.onRemoveSpot}
+ onDeploySpot={this.onDeploySpot}
+ />
+ :
+ 暂无热点图
+
+ }
+
+
说明:拖拽点位进行布设,拖出画布移除点位。
+ {
+ pictureInfo ?
+
+
{
+ this.props.dispatch(deleteGraph(pictureInfo.id)).then(_ => {
+ this.getData();
+ })
+ }}>
+ 删除图片
+
+
this.editHandler(pictureInfo)} style={{ marginLeft: 15 }}>修改图片
+
完成
+
+ :
+
+ 添加布设图
+
+ }
+
+
+
+
+ {this.state.uploadImgModal}
+
)
+ }
+}
+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));
\ No newline at end of file
diff --git a/web/client/src/sections/projectRegime/containers/pointDeploy/deploy-style.less b/web/client/src/sections/projectRegime/containers/pointDeploy/deploy-style.less
new file mode 100644
index 0000000..c61a5f1
--- /dev/null
+++ b/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;
+ }
+}
\ No newline at end of file
diff --git a/web/client/src/sections/projectRegime/containers/pointDeploy/upload-img-modal.js b/web/client/src/sections/projectRegime/containers/pointDeploy/upload-img-modal.js
new file mode 100644
index 0000000..8917fe4
--- /dev/null
+++ b/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 (
+
+
+
+
+
+ 说明:附件格式为png、jpeg、jpg,大小不超过10MB
+
+
+
+ )
+}
+
+function mapStateToProps(state) {
+ const { auth, global } = state;
+ return {
+ user: auth.user,
+ actions: global.actions,
+ }
+}
+
+export default connect(mapStateToProps)(DisclosureModal);
\ No newline at end of file
diff --git a/web/client/src/sections/projectRegime/routes.js b/web/client/src/sections/projectRegime/routes.js
index e722522..d553570 100644
--- a/web/client/src/sections/projectRegime/routes.js
+++ b/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',
diff --git a/web/client/src/utils/webapi.js b/web/client/src/utils/webapi.js
index 8232b71..79ad0a1 100644
--- a/web/client/src/utils/webapi.js
+++ b/web/client/src/utils/webapi.js
@@ -129,6 +129,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 = {
diff --git a/web/package.json b/web/package.json
index 1899540..2cd6963 100644
--- a/web/package.json
+++ b/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",