diff --git a/web/client/assets/images/menu/device.png b/web/client/assets/images/menu/device.png new file mode 100644 index 0000000..2550700 Binary files /dev/null and b/web/client/assets/images/menu/device.png differ diff --git a/web/client/src/app.js b/web/client/src/app.js index 1541740..c83c4e2 100644 --- a/web/client/src/app.js +++ b/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 ( ) diff --git a/web/client/src/sections/deviceManage/actions/device.js b/web/client/src/sections/deviceManage/actions/device.js new file mode 100644 index 0000000..0c96da3 --- /dev/null +++ b/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 || '设备编辑', + }, + }); +} + diff --git a/web/client/src/sections/deviceManage/actions/index.js b/web/client/src/sections/deviceManage/actions/index.js new file mode 100644 index 0000000..7ae21d1 --- /dev/null +++ b/web/client/src/sections/deviceManage/actions/index.js @@ -0,0 +1,7 @@ +'use strict'; + +import * as device from './device' + +export default { + ...device +} \ No newline at end of file diff --git a/web/client/src/sections/deviceManage/components/importDevicesModal.js b/web/client/src/sections/deviceManage/components/importDevicesModal.js new file mode 100644 index 0000000..bff485d --- /dev/null +++ b/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 ( { + setMsg('') + setLoading(false) + setPostData([]) + onCancel() + }} + > +
+ { + 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); + }}> + + + {msg} +
最大不超过200M,导入文件需与 + download()} style={{ cursor: 'pointer', color: '#0066FF' }}>导入模板 + 一致
+
+
) +} + +function mapStateToProps(state) { + const { auth, global } = state; + return { + user: auth.user, + actions: global.actions, + } +} + +export default connect(mapStateToProps)(ImportDeviceModal); \ No newline at end of file diff --git a/web/client/src/sections/deviceManage/components/modelModal.js b/web/client/src/sections/deviceManage/components/modelModal.js new file mode 100644 index 0000000..e2fe443 --- /dev/null +++ b/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 ( + + {title || ''} + + } + layout="horizontal" + grid={true} + {...formItemLayout} + modalProps={{ + destroyOnClose: true, + onCancel: () => { }, + }} + onFinish={async (values) => { + return onFinish && await onFinish(values, editData, form) + // return true; + }} + width={500} + > + + + + + { + return { label: s, value: s } + }) + } + name="type" + label="设备类型" + /> + + + + + + + + + + ); +}; + +export const DEVICE_TYPES = ['安防系统', '厨房系统', '电梯', '供电系统', '空调', '排水系统', '水系统', '通道门禁', + '通风系统', '通信系统', '显示视频', '消防系统', '照明系统'] \ No newline at end of file diff --git a/web/client/src/sections/deviceManage/constants/index.js b/web/client/src/sections/deviceManage/constants/index.js new file mode 100644 index 0000000..e69de29 diff --git a/web/client/src/sections/deviceManage/containers/deviceManage.js b/web/client/src/sections/deviceManage/containers/deviceManage.js new file mode 100644 index 0000000..8e42a1d --- /dev/null +++ b/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(编辑} + editData={record} + title="编辑设备" + onFinish={onFinish} + key="editModel" + />) + + options.push( + handleDelete(record.id)} + okText="是" + cancelText="否" + > + 删除 + ) + + return options; + + }, + }, + ]; + + return
+ +
+
+
+ + 设备管理 +  DEVICE +
+
+ 新建} + title="新建设备" + onFinish={onFinish} + key="addModel" + /> + + { + rowSelected?.length > 0 ? handleDelete(rowSelected?.toString()) : message.warning('请先选择要删除的设备') + }}> + + + + setName(e?.target?.value)} style={{ width: '13vw', marginLeft: 20, marginRight: 10 }} /> + +
+
+
+ { + return {`共${Math.ceil(total / pageSize)}页,${total}项`} + }, + 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 && { + setShowImportModal(false) + }} + onOk={() => { + setShowImportModal(false) + queryData() + }} + />} +
+
+ + +} + +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); + + diff --git a/web/client/src/sections/deviceManage/containers/index.js b/web/client/src/sections/deviceManage/containers/index.js new file mode 100644 index 0000000..4577014 --- /dev/null +++ b/web/client/src/sections/deviceManage/containers/index.js @@ -0,0 +1,5 @@ +'use strict'; + +import DeviceManage from './deviceManage' + +export { DeviceManage }; diff --git a/web/client/src/sections/deviceManage/containers/style.css b/web/client/src/sections/deviceManage/containers/style.css new file mode 100644 index 0000000..e69de29 diff --git a/web/client/src/sections/deviceManage/containers/style.less b/web/client/src/sections/deviceManage/containers/style.less new file mode 100644 index 0000000..e69de29 diff --git a/web/client/src/sections/deviceManage/index.js b/web/client/src/sections/deviceManage/index.js new file mode 100644 index 0000000..7a2f7a2 --- /dev/null +++ b/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 +}; \ No newline at end of file diff --git a/web/client/src/sections/deviceManage/nav-item.js b/web/client/src/sections/deviceManage/nav-item.js new file mode 100644 index 0000000..607aa65 --- /dev/null +++ b/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 ( + } + key="deviceManage"> + 设备管理 + + + ); +} \ No newline at end of file diff --git a/web/client/src/sections/deviceManage/reducers/index.js b/web/client/src/sections/deviceManage/reducers/index.js new file mode 100644 index 0000000..7ed1088 --- /dev/null +++ b/web/client/src/sections/deviceManage/reducers/index.js @@ -0,0 +1,5 @@ +'use strict'; + +export default { + +} \ No newline at end of file diff --git a/web/client/src/sections/deviceManage/routes.js b/web/client/src/sections/deviceManage/routes.js new file mode 100644 index 0000000..b57a687 --- /dev/null +++ b/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, + + } +}]; \ No newline at end of file diff --git a/web/client/src/sections/projectRegime/components/pointModel.js b/web/client/src/sections/projectRegime/components/pointModel.js index 2818258..45274ff 100644 --- a/web/client/src/sections/projectRegime/components/pointModel.js +++ b/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}> + s?.deviceId) || []}> + + { 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: '请选择结构物类型' },]} > + }
{ - 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) => { { 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 || [] }; } diff --git a/web/client/src/utils/webapi.js b/web/client/src/utils/webapi.js index 07f09da..b54b2f0 100644 --- a/web/client/src/utils/webapi.js +++ b/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 = {