Browse Source

(*)设备管理功能提交 点位增加绑定设备

master
peng.peng 1 year ago
parent
commit
e70bd12ba8
  1. BIN
      web/client/assets/images/menu/device.png
  2. 4
      web/client/src/app.js
  3. 56
      web/client/src/sections/deviceManage/actions/device.js
  4. 7
      web/client/src/sections/deviceManage/actions/index.js
  5. 213
      web/client/src/sections/deviceManage/components/importDevicesModal.js
  6. 111
      web/client/src/sections/deviceManage/components/modelModal.js
  7. 0
      web/client/src/sections/deviceManage/constants/index.js
  8. 254
      web/client/src/sections/deviceManage/containers/deviceManage.js
  9. 5
      web/client/src/sections/deviceManage/containers/index.js
  10. 0
      web/client/src/sections/deviceManage/containers/style.css
  11. 0
      web/client/src/sections/deviceManage/containers/style.less
  12. 15
      web/client/src/sections/deviceManage/index.js
  13. 13
      web/client/src/sections/deviceManage/nav-item.js
  14. 5
      web/client/src/sections/deviceManage/reducers/index.js
  15. 13
      web/client/src/sections/deviceManage/routes.js
  16. 12
      web/client/src/sections/projectRegime/components/pointModel.js
  17. 32
      web/client/src/sections/projectRegime/components/projectAddModel.js
  18. 7
      web/client/src/sections/projectRegime/containers/point.js
  19. 5
      web/client/src/utils/webapi.js

BIN
web/client/assets/images/menu/device.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

4
web/client/src/app.js

@ -9,6 +9,8 @@ import Organization from './sections/organization';
import PatrolManage from './sections/patrolManage';
import IssueHandle from './sections/issueHandle'
import Shouye from './sections/shouye';
import DeviceManage from './sections/deviceManage';
import { Func } from '$utils';
const App = props => {
const { projectName } = props
@ -20,7 +22,7 @@ const App = props => {
return (
<Layout
title={projectName}
sections={[Auth,Shouye, ProjectRegime, Safetymanage, Organization, PatrolManage, IssueHandle]}
sections={[Auth, Shouye, ProjectRegime, Safetymanage, Organization, PatrolManage, IssueHandle, DeviceManage]}
/>
)

56
web/client/src/sections/deviceManage/actions/device.js

@ -0,0 +1,56 @@
'use strict';
import { basicAction } from '@peace/utils'
import { ApiTable } from '$utils'
export function getDeviceList(query) {
return dispatch => basicAction({
type: 'get',
dispatch: dispatch,
query: query || {},
actionType: 'GET_Device_REPORT',
url: `${ApiTable.getDeviceList}`,
msg: { error: '获取设备列表失败' },
reducer: { name: 'device' }
});
}
export function addDevice(params) {
return (dispatch) => basicAction({
type: 'post',
data: params,
dispatch,
actionType: 'ADD_Device_REPORT',
url: ApiTable.addDevice,
msg: {
option: '设备新增',
},
});
}
export function deleteDevice(id) {
return (dispatch) => basicAction({
type: 'del',
dispatch,
actionType: 'DELETE_Device_REPORT',
url: ApiTable.modifyDevice.replace('{id}', id),
msg: {
option: '设备删除',
},
});
}
export function modifyDevice(id, params, msg) {
return (dispatch) => basicAction({
type: 'put',
data: params,
dispatch,
actionType: 'MODIFY_Device_REPORT',
url: ApiTable.modifyDevice.replace('{id}', id),
msg: {
option: msg || '设备编辑',
},
});
}

7
web/client/src/sections/deviceManage/actions/index.js

@ -0,0 +1,7 @@
'use strict';
import * as device from './device'
export default {
...device
}

213
web/client/src/sections/deviceManage/components/importDevicesModal.js

@ -0,0 +1,213 @@
'use strict';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';
import { Modal, Form, Button, Upload, message } from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
import XLSX from 'xlsx';
import { DEVICE_TYPES } from './modelModal';
// import porvince from './province.json'
const workerKeys = {
name: '设备名称',
type: '设备类型',
specification: '规格型号',
dateProduced: '生产日期',
dateInstall: '安装时间',
dateGuarantee: '质保期',
dateMainten: '维保期',
}
//下载模板和上传文件读取
const ImportDeviceModal = props => {
const { dispatch, actions, onCancel, onOk, devices } = props;
const { deviceManage } = actions;
const [msg, setMsg] = useState('');
const [loading, setLoading] = useState(false);
const [postData, setPostData] = useState([]);
//初始化
useEffect(() => {
}, []);
const confirm = () => {
if (postData.length) {
setLoading(true)
//导入明细接口通用
dispatch(deviceManage?.addDevice(postData)).then(res => {
if (res.success) {
onOk()
}
setLoading(false)
})
} else {
message.warning({ content: '没有数据可以提交,请上传数据文件', duration: 2 })
}
}
const dldCsvMb = () => {
//表头
let head = [];
Object.keys(workerKeys).map(key => {
head.push(workerKeys[key]);
})
head = head.join(',') + "\n";
//数据
//let data = 1 + ',' + 2 + ',' + 3 + ',' + 4 + ',' + 5
let templateCsv = "data:text/xls;charset=utf-8,\ufeff" + head;
//创建一个a标签
let link = document.createElement("a");
//为a标签设置属性
link.setAttribute("href", templateCsv);
link.setAttribute("download", `设备导入模板.xls`);
//点击a标签
link.click();
}
const download = () => {
dldCsvMb();
}
const judgeTimeValid = (v) => {
let valid = true;
if (v.split('/').length !== 3) {
valid = false;
} else {
let time = new Date(v);
// if (!time) {
// return valid;//可以不填
// }
const ymd = /^((19|20)[0-9]{2})[\/\-]((0[1-9])|(1[0-2]))[\/\-]((0[1-9])|((1|2)[0-9])|(3[0-1]))$/;//年月日
if (time instanceof Date) {
let timeStr = moment(time).format('YYYY/MM/DD');
if (!ymd.test(timeStr)) {
valid = false;
}
} else {
valid = false;
}
}
return valid;
}
return (<Modal
title={"导入设备信息"} visible={true}
onOk={confirm} width={520}
confirmLoading={loading}
onCancel={() => {
setMsg('')
setLoading(false)
setPostData([])
onCancel()
}}
>
<Form>
<Upload
action={'/'} accept={'.xls,.xlsx'}
maxCount={1}
onRemove={(currentFile, fileList, fileItem) => {
setMsg('');
setPostData([]);
}}
customRequest={async (data) => {
const { file, onSuccess, onError } = data
const error = (msg) => {
setMsg(msg)
onError({ message: msg })
}
let isLt = file.size / 1024 / 1024 < 200
if (!isLt) {
error(`文件最大不超过200M`);
return;
}
const reader = new FileReader();
reader.onload = function (e) {
const data = e.target.result;
const workbook = XLSX.read(data, { type: 'binary', codepage: 936 });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
//XLSX.utils.sheet_to_json解析 ------把身份证号和手机号变成 字符串------
Object.keys(worksheet).forEach(k => {
worksheet[k].w ? worksheet[k].v = worksheet[k].w : ''
})
//------------------end------------------
const res = XLSX.utils.sheet_to_json(worksheet);
if (res.length > 1000) {
error('一次性上传数据行数应小于1000行,请分批上传')
return
}
if (!res.length) {
error('请填写至少一行数据')
return
}
let postData = []
for (let i = 0; i < res.length; i++) {
let d = res[i]
let obj = {};
Object.keys(workerKeys).map(key => {
obj[key] = d[workerKeys[key]] || null;
})
//必填项
let notNullKeys = ['name', 'type', 'specification', 'dateProduced', 'dateGuarantee', 'dateMainten', 'dateInstall']
for (let k = 0; k < notNullKeys.length; k++) {
let key = notNullKeys[k];
if (!obj[key]) {
error(`${i + 2}行【${workerKeys[key]}】不能为空`)
return
}
}
//判断设备类型
let ext = DEVICE_TYPES.find(m => m == obj.type);
if (!ext) {
error(`${i + 2}行的【设备类型】错误,请填写【${DEVICE_TYPES.toString()}】中的一种`)
return
}
let tValid = judgeTimeValid(obj.dateProduced) && judgeTimeValid(obj.dateGuarantee)
&& judgeTimeValid(obj.dateMainten) && judgeTimeValid(obj.dateInstall);
if (!tValid) {
error(`${i + 2}行【日期格式】错误,请填写yyyy/mm/dd格式`)
return;
}
let dateInstallValid = moment(obj.dateInstall).valueOf() > moment().startOf('d').add(1, 'd').valueOf();
if (dateInstallValid) {
error(`${i + 2}行【安装日期】不能填写今天之后的时间`)
return;
}
postData.push(obj)
}
setPostData(postData)
let msg = '文件解析完成,点击确定按钮上传保存!'
setMsg(msg)
onSuccess({ message: msg })
}
reader.readAsBinaryString(file);
}}>
<Button icon={<DownloadOutlined />} theme="light">
请选择文件
</Button>
</Upload>
<span>{msg}</span>
<div style={{ color: '#ccc', marginTop: 20 }}>最大不超过200M导入文件需与
<span onClick={() => download()} style={{ cursor: 'pointer', color: '#0066FF' }}>导入模板</span>
一致</div>
</Form>
</Modal>)
}
function mapStateToProps(state) {
const { auth, global } = state;
return {
user: auth.user,
actions: global.actions,
}
}
export default connect(mapStateToProps)(ImportDeviceModal);

111
web/client/src/sections/deviceManage/components/modelModal.js

@ -0,0 +1,111 @@
import React, { useRef } from 'react';
import { Button, Form } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import {
ModalForm,
ProFormSelect,
ProFormText,
ProFormDatePicker
} from '@ant-design/pro-form';
import moment from 'moment';
export default (props) => {
const { title, triggerRender, editData = null, onFinish, devices } = props;
const formItemLayout = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
const initialValues = editData ? {
...editData,
} : {};
const [form] = Form.useForm();
const formRef = useRef();
const disabledDate = (value) => {
return value.valueOf() > moment().startOf('d').add(1, 'd').valueOf();
}
return (
<ModalForm
formRef={formRef}
title={title || ''}
initialValues={initialValues}
trigger={
triggerRender ? triggerRender : <Button type="primary" >
{title || ''}
</Button>
}
layout="horizontal"
grid={true}
{...formItemLayout}
modalProps={{
destroyOnClose: true,
onCancel: () => { },
}}
onFinish={async (values) => {
return onFinish && await onFinish(values, editData, form)
// return true;
}}
width={500}
>
<ProFormText
rules={[
{ required: true, message: '请输入设备名称' },
{ max: 255, message: '设备名称长度不能大于255个字符' },
]}
name="name"
label="设备名称"
/>
<ProFormSelect
rules={[{ required: true, message: '请选择设备类型' }]}
options={
DEVICE_TYPES
.map(s => {
return { label: s, value: s }
})
}
name="type"
label="设备类型"
/>
<ProFormText
rules={[
{ required: true, message: '请输入规格型号' },
{ max: 20, message: '规格型号长度不能大于20个字符' },
]}
name="specification"
label="规格型号"
/>
<ProFormDatePicker
rules={[{ required: true, message: '请输入生产日期' }]}
name="dateProduced"
label="生产日期"
fieldProps={
{ disabledDate: disabledDate }
}
/>
<ProFormDatePicker
rules={[{ required: true, message: '请输入安装日期' }]}
name="dateInstall"
label="安装日期"
fieldProps={
{ disabledDate: disabledDate }
}
/>
<ProFormDatePicker
rules={[{ required: true, message: '请输入质保期' }]}
name="dateGuarantee"
label="质保期"
/>
<ProFormDatePicker
rules={[{ required: true, message: '请输入维保期' }]}
name="dateMainten"
label="维保期"
/>
</ModalForm>
);
};
export const DEVICE_TYPES = ['安防系统', '厨房系统', '电梯', '供电系统', '空调', '排水系统', '水系统', '通道门禁',
'通风系统', '通信系统', '显示视频', '消防系统', '照明系统']

0
web/client/src/sections/deviceManage/constants/index.js

254
web/client/src/sections/deviceManage/containers/deviceManage.js

@ -0,0 +1,254 @@
import React, { useEffect, useState } from 'react'
import { Spin, Popconfirm, message, Button, Input } from 'antd';
import { connect } from 'react-redux';
import ProTable from '@ant-design/pro-table';
import DeviceModal from '../components/modelModal'
import moment from 'moment';
import ImportDeviceModal from '../components/importDevicesModal'
function DeviceManagement(props) {
const { loading, clientHeight, actions, dispatch, devices } = props;
const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [rowSelected, setRowSelected] = useState([])
const [showImportModal, setShowImportModal] = useState(false);
const [name, setName] = useState();
const queryData = (search) => {
const query = {
limit: search ? 10 : pageSize || 10,
page: search ? 1 : currentPage || 1,
name: name
}
dispatch(actions.deviceManage.getDeviceList(query));
}
useEffect(() => {
queryData();
}, [pageSize, currentPage]);
const handleDelete = (id) => {
dispatch(actions.deviceManage.deleteDevice(id)).then(() => {
queryData();
setRowSelected([])
});
};
const onFinish = async (values, editData) => {
if (editData) {
const dataToSave = { ...values }
return dispatch(
actions.deviceManage.modifyDevice(editData.id, dataToSave, values?.msg || ''),
).then((res) => {
if (res.success) {
queryData();
return true;
} else {
return false;
}
});
}
return dispatch(actions.deviceManage.addDevice({
...values,
})).then(res => {
if (res.success) {
queryData();
return true;
} else {
return false;
}
});
};
const columns = [
{
title: '设备名称',
dataIndex: 'name',
ellipsis: true,
},
{
title: '设备类型',
dataIndex: 'type',
ellipsis: true,
},
{
title: '规格型号',
dataIndex: 'specification',
ellipsis: true,
},
{
title: '生产日期',
dataIndex: 'dateProduced',
ellipsis: true,
},
{
title: '安装时间',
dataIndex: 'dateInstall',
ellipsis: true,
},
{
title: '质保期',
dataIndex: 'dateGuarantee',
ellipsis: true,
search: false,
},
{
title: '维保期',
dataIndex: 'dateMainten',
ellipsis: true,
},
{
title: '设备投入使用时长',
dataIndex: 'length',
ellipsis: true,
render: (text, record) => {
const start = moment(record?.dateInstall);
const end = moment();
const days = end.diff(start, 'days');
return days + '天'
}
},
{
title: '操作',
width: 160,
key: 'option',
valueType: 'option',
render: (text, record) => {
const options = [];
options.push(<DeviceModal
triggerRender={<a>编辑</a>}
editData={record}
title="编辑设备"
onFinish={onFinish}
key="editModel"
/>)
options.push(
<Popconfirm
key="del"
placement="top"
title="是否确认删除该设备?"
onConfirm={() => handleDelete(record.id)}
okText="是"
cancelText="否"
>
<a>删除</a>
</Popconfirm>)
return options;
},
},
];
return <div id='patrol-record' className='global-main'>
<Spin spinning={loading}>
<div style={{ marginBottom: 19 }}>
<div className='top' style={{ marginBottom: 19 }}>
<div className='title'>
<span className='line'></span>
<span className='cn'>设备管理</span>
<span className='en'>&nbsp;DEVICE</span>
</div>
<div>
<DeviceModal
triggerRender={<Button type='primary'>新建</Button>}
title="新建设备"
onFinish={onFinish}
key="addModel"
/>
<Button type="primary" style={{ marginRight: 10, marginLeft: 10 }} onClick={() => { setShowImportModal(true) }}>批量新增</Button>
<Popconfirm title="确认删除?" onConfirm={() => {
rowSelected?.length > 0 ? handleDelete(rowSelected?.toString()) : message.warning('请先选择要删除的设备')
}}>
<Button>批量删除</Button>
</Popconfirm>
<Input onChange={e => setName(e?.target?.value)} style={{ width: '13vw', marginLeft: 20, marginRight: 10 }} />
<Button type="primary" onClick={() => {
setPageSize(10)
setCurrentPage(1)
queryData(true)
}}>查询</Button>
</div>
</div>
</div>
<ProTable
columns={columns}
rowKey="id"
dateFormatter="string"
scroll={
{
scrollToFirstRowOnChange: true,
y: clientHeight - 260
}
}
pagination={{
size: 'large',
total: devices?.count,
showSizeChanger: true,
// showQuickJumper: true,
current: currentPage,
pageSize: pageSize || 10,
pageSizeOptions: [10, 20, 50],
showTotal: (total) => {
return <span style={{ fontSize: 15 }}>{`${Math.ceil(total / pageSize)}页,${total}`}</span>
},
onShowSizeChange: (currentPage, pageSize) => {
setCurrentPage(currentPage);
setPageSize(pageSize);
},
onChange: (page, pageSize) => {
setCurrentPage(page);
setPageSize(pageSize);
}
}}
dataSource={devices?.rows || []}
rowSelection={{
selectedRowKeys: rowSelected,
onChange: (selectedRowKeys) => {
setRowSelected(selectedRowKeys);
},
getCheckboxProps: (record) => {
return {
disabled: record.username === 'SuperAdmin',
}
}
}}
options={false}
search={false}
/>
{showImportModal && <ImportDeviceModal
devices={devices?.rows || []}
onCancel={() => {
setShowImportModal(false)
}}
onOk={() => {
setShowImportModal(false)
queryData()
}}
/>}
</Spin>
</div>
}
function mapStateToProps(state) {
const {
auth, global, device
} = state;
return {
loading: device.isRequesting,
clientHeight: global.clientHeight,
actions: global.actions,
devices: device?.data || {}
};
}
export default connect(mapStateToProps)(DeviceManagement);

5
web/client/src/sections/deviceManage/containers/index.js

@ -0,0 +1,5 @@
'use strict';
import DeviceManage from './deviceManage'
export { DeviceManage };

0
web/client/src/sections/deviceManage/containers/style.css

0
web/client/src/sections/deviceManage/containers/style.less

15
web/client/src/sections/deviceManage/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: 'deviceManage',
name: '设备管理',
reducers: reducers,
routes: routes,
actions: actions,
getNavItem: getNavItem
};

13
web/client/src/sections/deviceManage/nav-item.js

@ -0,0 +1,13 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Menu } from 'antd';
import { Func } from '$utils';
export function getNavItem(user, dispatch) {
return (
<Menu.Item icon={<img src='/assets/images/menu/device.png' style={{ width: 24, height: 24 }} />}
key="deviceManage">
<Link to="/deviceManage">设备管理</Link>
</Menu.Item>
);
}

5
web/client/src/sections/deviceManage/reducers/index.js

@ -0,0 +1,5 @@
'use strict';
export default {
}

13
web/client/src/sections/deviceManage/routes.js

@ -0,0 +1,13 @@
'use strict';
import { DeviceManage } from './containers';
export default [{
type: 'inner',
route: {
path: '/deviceManage',
key: 'deviceManage',
breadcrumb: '设备管理',
component: DeviceManage,
}
}];

12
web/client/src/sections/projectRegime/components/pointModel.js

@ -7,7 +7,7 @@ import Uploads from '$components/Uploads';
import { useEffect } from 'react';
import moment from 'moment';
const ProjectAddModel = ({ dispatch, actions, user, modelData, close, success, qrCodeId }) => {
const ProjectAddModel = ({ dispatch, actions, user, modelData, close, success, qrCodeId, devices }) => {
const { projectRegime } = actions
const [showBaiduMap, setShowBaiduMap] = useState(false)
@ -172,6 +172,16 @@ const ProjectAddModel = ({ dispatch, actions, user, modelData, close, success, q
initialValue={modelData?.equipmentModel}>
<Input placeholder="请输入设备型号" allowClear />
</Form.Item>
<Form.Item label='设备绑定' name="devices" style={{}}
initialValue={modelData?.pointDevices?.map(s => s?.deviceId) || []}>
<Select mode="multiple">
{
devices?.map(s => <Select.Option
disabled={s?.pointDevices?.length > 0 && !s?.pointDevices?.find(x => x.pointId == modelData?.id)}
value={s.id} >{s?.name}</Select.Option>)
}
</Select>
</Form.Item>
<Form.Item
label="点位图片"
name='img'

32
web/client/src/sections/projectRegime/components/projectAddModel.js

@ -6,11 +6,15 @@ import { connect } from 'react-redux';
import Uploads from '$components/Uploads';
import { useEffect } from 'react';
// import moment from 'moment';
const type_options = [
{ value: '桥梁', label: '桥梁' },
{ value: '隧道', label: '隧道' },
{ value: '管廊', label: '管廊' }]
const ProjectAddModel = ({ dispatch, actions, user, modelData, close, success, firmList }) => {
const { projectRegime } = actions
const [showBaiduMap, setShowBaiduMap] = useState(false)
const [type, setType] = useState(modelData?.type || type_options[0])
const [form] = Form.useForm();
useEffect(() => {
@ -103,15 +107,33 @@ const ProjectAddModel = ({ dispatch, actions, user, modelData, close, success, f
// rules={[{ required: true, message: '请选择结构物类型' },]}
>
<Select
onChange={value => { setType(value) }}
bordered={false}
allowClear
options={[
{ value: '桥梁', label: '桥梁' },
{ value: '隧道', label: '隧道' },
{ value: '管廊', label: '管廊' }]}
options={type_options}
/>
</Form.Item>
</div>
{type == '管廊' && <Form.Item label='子系统' name="subType"
initialValue={modelData?.subType || '指挥中心'}
rules={[{ required: true, message: '请选择子系统' }]}
>
<Select
bordered={false}
options={[
{ value: '指挥中心', label: '指挥中心' },
{ value: '管廊本体', label: '管廊本体' },
{ value: '电梯系统', label: '电梯系统' },
{ value: '供配电系统', label: '供配电系统' },
{ value: '防雷与接地系统', label: '防雷与接地系统' },
{ value: '燃气仓', label: '燃气仓' },
{ value: '给水仓', label: '给水仓' },
{ value: '电气仓', label: '电气仓' },
{ value: '安防系统', label: '安防系统' },
{ value: '高压电力仓', label: '高压电力仓' },
]}
/>
</Form.Item>}
<div style={{ position: 'relative' }}>
<Form.Item label="所在地区:" labelCol={{ span: 9 }} labelAlign='right' name="longitude" style={{ display: 'inline-block', width: 'calc(60% - 50px)', }}
rules={[{ required: true, message: '', }, {

7
web/client/src/sections/projectRegime/containers/point.js

@ -5,7 +5,7 @@ import '../style.less';
import PointModel from '../components/pointModel'
const Information = (props) => {
const { dispatch, actions } = props
const { dispatch, actions, devices } = props
const { projectRegime } = actions
const [tableList, settableList] = useState([])
const [addModel, setAddModel] = useState(false)
@ -28,6 +28,7 @@ const Information = (props) => {
const projectList = (obj) => {
const { limit, page } = obj
dispatch(actions.deviceManage.getDeviceList());
dispatch(projectRegime.positionList({ limit, page: 0, projectId: qrCodeId })).then(res => {
if (res.success) {
let data = []
@ -166,6 +167,7 @@ const Information = (props) => {
<PointModel
modelData={modelData}
qrCodeId={qrCodeId}
devices={devices}
close={() => {
setAddModel(false)
setModelData({})
@ -183,10 +185,11 @@ const Information = (props) => {
}
function mapStateToProps(state) {
const { auth, global } = state;
const { auth, global, device } = state;
return {
user: auth.user,
actions: global.actions,
devices: device?.data?.rows || []
};
}

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

@ -139,6 +139,11 @@ export const ApiTable = {
getProjectPoints: 'project/{projectId}/all/points',
getDeployPoints: 'picture/{pictureId}/deploy/points',
setDeployPoints: 'set/picture/{pictureId}/deploy/points',
//设备管理
getDeviceList: 'device',
addDevice: 'device',
modifyDevice: 'device/{id}',
};
export const RouteTable = {

Loading…
Cancel
Save