+
)
}
@@ -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/sections/shouye/actions/index.js b/web/client/src/sections/shouye/actions/index.js
new file mode 100644
index 0000000..e603f22
--- /dev/null
+++ b/web/client/src/sections/shouye/actions/index.js
@@ -0,0 +1,9 @@
+'use strict';
+
+
+
+
+export default {
+
+
+}
\ No newline at end of file
diff --git a/web/client/src/sections/shouye/containers/index.js b/web/client/src/sections/shouye/containers/index.js
new file mode 100644
index 0000000..3923e11
--- /dev/null
+++ b/web/client/src/sections/shouye/containers/index.js
@@ -0,0 +1,9 @@
+'use strict';
+
+
+
+import Shouye from './shouye'
+
+
+
+export { Shouye };
diff --git a/web/client/src/sections/shouye/containers/shouye.js b/web/client/src/sections/shouye/containers/shouye.js
new file mode 100644
index 0000000..a07a517
--- /dev/null
+++ b/web/client/src/sections/shouye/containers/shouye.js
@@ -0,0 +1,62 @@
+import React, { useEffect, useState } from 'react';
+import { connect } from 'react-redux';
+import { Spin, Card, Form, Input, Select, Button, Table, Modal, Popconfirm, Tooltip } from 'antd';
+import moment from "moment";
+import '../style.less';
+import { push } from 'react-router-redux';
+import { Model } from 'echarts';
+
+
+const Information = (props) => {
+ const { dispatch, actions, user, loading } = props
+ const topdata =[]
+
+
+
+ return (
+ <>
+
+ >
+ )
+}
+
+function mapStateToProps (state) {
+ const { auth, global } = state;
+ return {
+ user: auth.user,
+ actions: global.actions,
+ };
+}
+
+export default connect(mapStateToProps)(Information);
diff --git a/web/client/src/sections/shouye/index.js b/web/client/src/sections/shouye/index.js
new file mode 100644
index 0000000..319442b
--- /dev/null
+++ b/web/client/src/sections/shouye/index.js
@@ -0,0 +1,15 @@
+'use strict';
+
+import reducers from './reducers';
+import routes from './routes';
+import actions from './actions';
+import { getNavItem } from './nav-item';
+
+export default {
+ key: 'shouye',
+ name: '首页',
+ reducers: reducers,
+ routes: routes,
+ actions: actions,
+ getNavItem: getNavItem
+};
\ No newline at end of file
diff --git a/web/client/src/sections/shouye/nav-item.js b/web/client/src/sections/shouye/nav-item.js
new file mode 100644
index 0000000..0de1336
--- /dev/null
+++ b/web/client/src/sections/shouye/nav-item.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Menu } from 'antd';
+import { SettingOutlined } from '@ant-design/icons';
+import { Func } from '$utils';
+const SubMenu = Menu.SubMenu;
+
+export function getNavItem (user, dispatch) {
+ // return
} title={'首页'}>
+ // {/* {Func.isAuthorized('STRU_INFO_CONFIG') &&
+ // 结构物基础信息管理
+ // }
+ // {Func.isAuthorized('QR_CODE_CONFIG') &&
+ // 二维码管理
+ // } */}
+ //
+ return
+ 首页
+
+
+}
\ No newline at end of file
diff --git a/web/client/src/sections/shouye/reducers/index.js b/web/client/src/sections/shouye/reducers/index.js
new file mode 100644
index 0000000..7ed1088
--- /dev/null
+++ b/web/client/src/sections/shouye/reducers/index.js
@@ -0,0 +1,5 @@
+'use strict';
+
+export default {
+
+}
\ No newline at end of file
diff --git a/web/client/src/sections/shouye/routes.js b/web/client/src/sections/shouye/routes.js
new file mode 100644
index 0000000..73ae24b
--- /dev/null
+++ b/web/client/src/sections/shouye/routes.js
@@ -0,0 +1,32 @@
+'use strict';
+import { Shouye } from './containers';
+
+export default [{
+ type: 'inner',
+ route: {
+ path: '/shouye',
+ key: 'shouye',
+ breadcrumb: '首页',
+ component: Shouye,
+ // 不设置 component 则面包屑禁止跳转
+ // childRoutes: [{
+ // path: '/information',
+ // key: 'information',
+ // breadcrumb: '结构物基础信息管理',
+ // component: Information,
+ // childRoutes: [ {
+ // path: '/:id',
+ // key: ':id',
+ // component: Point,
+ // breadcrumb: '点位',
+ // },
+ // ]
+ // }, {
+ // path: '/qrCode',
+ // key: 'qrCode',
+ // component: QrCode,
+ // breadcrumb: '二维码管理',
+ // },
+ // ]
+ }
+}];
\ No newline at end of file
diff --git a/web/client/src/sections/shouye/style.less b/web/client/src/sections/shouye/style.less
new file mode 100644
index 0000000..1e0398d
--- /dev/null
+++ b/web/client/src/sections/shouye/style.less
@@ -0,0 +1,20 @@
+.shouyetop{
+ display: flex;
+ justify-content: space-between;
+ .shouyetopitem{
+ width: 25%;
+ display: flex;
+ justify-content: space-between;
+ box-shadow: 0 0 10px #F0F2F5;
+ border:1px solid #F0F2F5;
+ color: rgba(0, 0, 0, 0.45);
+ font-size: 1.875rem;
+ height: 7.125rem;
+ .shouyetopitem-left{
+ width: 50%;
+ }
+ .shouyetopitem-right{
+ width: 50%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/client/src/utils/webapi.js b/web/client/src/utils/webapi.js
index 8232b71..bcf1c7e 100644
--- a/web/client/src/utils/webapi.js
+++ b/web/client/src/utils/webapi.js
@@ -64,6 +64,7 @@ export const ApiTable = {
delCheckTask: '/delcheckTask/:id',
addPatrolRecordIssueHandle: 'patrolRecord/issue/handle',
modifyPatrolRecordIssueHandle: 'patrolRecord/issue/handle/{id}',
+ yujingguanli:'/yujingguanli',
//协调申请
getCoordinateList: 'risk/coordinate',
@@ -129,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 = {
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",