diff --git a/package.json b/package.json index 05ffbd8..28a73b4 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,14 @@ "@cqsjjb/jjb-react-admin-component": "latest", "ahooks": "^3.9.5", "antd": "^5.27.6", + "antd-mobile": "^5.42.3", + "antd-mobile-icons": "^0.3.0", "dayjs": "^1.11.7", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", - "zy-react-library": "^1.1.41" + "react-signature-canvas": "^1.1.0-alpha.2", + "zy-react-library": "^1.1.42" }, "devDependencies": { "@antfu/eslint-config": "^5.4.1", diff --git a/router.md b/router.md index 561f831..82d7ad5 100644 --- a/router.md +++ b/router.md @@ -43,4 +43,8 @@ - `/primeport/container/stakeholder/personnelApplication/list` 人员申请 - `/primeport/container/stakeholder/vehicleApplication/list` 车辆申请 - `/primeport/container/stakeholder/personnelApplicationRecords/list` 人员申请记录 -- `/primeport/container/stakeholder/vehicleApplicationRecords/list` 车辆申请记录 \ No newline at end of file +- `/primeport/container/stakeholder/vehicleApplicationRecords/list` 车辆申请记录 + +### H5端 +- `/primeport/container/mobile/firstLevelDoor/personnelApplication/apply` 一级口门管理/人员申请/申请 +- `/primeport/container/mobile/firstLevelDoor/personnelApplication/applyList` 一级口门管理/人员申请/申请记录 diff --git a/src/api/global/index.js b/src/api/global/index.js index ba35796..444a64f 100644 --- a/src/api/global/index.js +++ b/src/api/global/index.js @@ -1,11 +1,7 @@ +import { declareRequest } from "@cqsjjb/jjb-dva-runtime"; + export {}; -// export const riskList = declareRequest( -// "loading", -// "Post > @/xxx", -// "dataSource: [] | res.data || [] & total: 0 | res.totalCount || 0 & pageIndex: 1 | res.pageIndex || 1 & pageSize: 10 | res.pageSize || 10", -// ); -// export const riskDelete = declareRequest( -// "loading", -// "Delete > @/xxx/{id}", -// ); +export const getDepartmentListTree = declareRequest( + "Post > @/basicInfo/department/listAllTreeByCorpType", +); diff --git a/src/components/SignatureH5/index.js b/src/components/SignatureH5/index.js new file mode 100644 index 0000000..c1345d7 --- /dev/null +++ b/src/components/SignatureH5/index.js @@ -0,0 +1,161 @@ +import { Image as AntdImage, Button, Popup, Toast } from "antd-mobile"; +import dayjs from "dayjs"; +import { useEffect, useRef, useState } from "react"; +import SignatureCanvas from "react-signature-canvas"; +import { base642File } from "zy-react-library/utils"; + +const SignatureH5 = (props) => { + const [signatureModalOpen, setSignatureModalOpen] = useState(false); + const signatureCanvas = useRef(null); + const [base64, setBase64] = useState(""); + const [signatureClientHeight, setSignatureClientHeight] = useState(""); + const [signatureClientWidth, setSignatureClientWidth] = useState(""); + + useEffect(() => { + if (signatureModalOpen) { + // setSignatureClientHeight(document.querySelector(".adm-popup-body").clientHeight - 50); + // setSignatureClientWidth(document.querySelector(".adm-popup-body").clientWidth - 100); + setSignatureClientHeight(window.innerHeight - 50); + setSignatureClientWidth(window.innerWidth - 100); + } + }, [signatureModalOpen]); + useEffect(() => { + setBase64(props.url); + }, [props.url]); + + const rotateBase64Img = (src) => { + return new Promise((resolve) => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + let imgW; + let imgH; + let size; + const cutCor = { sx: 0, sy: 0, ex: 0, ey: 0 }; + const image = new Image(); + image.crossOrigin = "anonymous"; + image.src = src; + image.onload = function () { + imgW = image.width; + imgH = image.height; + size = imgW > imgH ? imgW : imgH; + canvas.width = size * 2; + canvas.height = size * 2; + cutCor.sx = size; + cutCor.sy = size - imgW; + cutCor.ex = size + imgH; + cutCor.ey = size + imgW; + ctx.translate(size, size); + ctx.rotate((270 * Math.PI) / 180); + // ctx.scale(0.16, 0.16); + ctx.drawImage(image, 0, 0); + const imgData = ctx.getImageData( + cutCor.sx, + cutCor.sy, + cutCor.ex, + cutCor.ey, + ); + canvas.width = imgH; + canvas.height = imgW; + ctx.putImageData(imgData, 0, 0); + resolve(canvas.toDataURL("image/png")); + }; + }); + }; + + const onOk = async () => { + if (signatureCanvas.current.isEmpty()) { + Toast.show({ + icon: "fail", + content: "请签名", + }); + return; + } + const result = signatureCanvas.current.toDataURL(); + const base64 = await rotateBase64Img(result); + setBase64(base64); + props.onConfirm?.({ + time: dayjs().format("YYYY-MM-DD HH:mm:ss"), + base64, + file: base642File(base64), + }); + signatureCanvas.current.clear(); + setSignatureModalOpen(false); + }; + + return ( +
+
+ +
+ {base64 && ( +
+ +
+ )} + +
+
+ + + +
+
+ +
+
+ 请横屏签字 +
+
+
+
+ ); +}; + +export default SignatureH5; diff --git a/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/Apply/index.js b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/Apply/index.js new file mode 100644 index 0000000..9d759ce --- /dev/null +++ b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/Apply/index.js @@ -0,0 +1,514 @@ +import { Connect } from "@cqsjjb/jjb-dva-runtime"; +import { + Button, + Cascader, + Checkbox, + DatePicker, + Form, + Image, + ImageUploader, + Input, + Picker, + Popup, + Toast, +} from "antd-mobile"; +import dayjs from "dayjs"; +import { useEffect, useRef, useState } from "react"; +import useDictionary from "zy-react-library/hooks/useDictionary"; +import { ID_NUMBER, LICENSE_PLATE_NUMBER, PHONE } from "zy-react-library/regular"; +import { validatorEndTime } from "zy-react-library/utils"; +import SignatureH5 from "~/components/SignatureH5"; +import { NS_GLOBAL } from "~/enumerate/namespace"; +import "../../../index.less"; + +function Apply(props) { + const [primeportAreaList, setPrimeportAreaList] = useState([]); + const [vehicleTypeList, setVehicleTypeList] = useState([]); + const [licensePlateTypeList, setLicensePlateTypeList] = useState([]); + const [departmentList, setDepartmentList] = useState([]); + const [noticePopupVisible, setNoticePopupVisible] = useState(false); + + const [signatureUrl, setSignatureUrl] = useState(""); + + const checkboxRef = useRef(null); + + const [form] = Form.useForm(); + const accessType = Form.useWatch("accessType", form); + const doorType = Form.useWatch("doorType", form); + const { getDictionary } = useDictionary(); + + const transformDepartmentList = (list) => { + return list.map(item => ({ + label: item.name, + value: item.id, + children: item.childrenList ? transformDepartmentList(item.childrenList) : undefined, + })); + }; + + const getData = async () => { + const primeportAreaData = await getDictionary({ dictValue: "primeport_area" }); + setPrimeportAreaList(primeportAreaData); + const vehicleTypeData = await getDictionary({ dictValue: "VEHICLE_TYPE" }); + setVehicleTypeList(vehicleTypeData); + const licensePlateTypeData = await getDictionary({ dictValue: "LICENSE_PLATE_TYPE" }); + setLicensePlateTypeList(licensePlateTypeData); + const { data: departmentList } = await props["getDepartmentListTree"]({ enterpriseType: [1, 2] }); + const transformedDepartmentList = transformDepartmentList(departmentList); + setDepartmentList(transformedDepartmentList); + }; + + useEffect(() => { + getData(); + }, []); + + const onFinish = (values) => { + if (!values.safetyNoticeAgreed || !values.signature) { + Toast.show({ + icon: "fail", + content: "请勾选《安全进港须知》并签字", + }); + return; + } + console.log(values); + props.history.push(`./success?accessType=${values.accessType}&text=${values.accessType === "1" ? values.licensePlateNumber : values.phoneNumber}`); + }; + + return ( +
+
+ 提交申请 + + )} + > + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + + {value => value?.[0]?.label || "请选择访问类型"} + + + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + + {value => value?.[0]?.label || "请选择选择申请口门"} + + + { + accessType === "1" + && ( + <> + + + + + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + ({ label: item.dictLabel, value: item.dictValue }))]} + > + {value => value?.[0]?.label || "请选择车辆类型"} + + + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + ({ label: item.dictLabel, value: item.dictValue }))]} + > + {value => value?.[0]?.label || "请选择车牌类型"} + + + + + + + + + ({ url: URL.createObjectURL(file), file })} + maxCount={2} + /> + + + ({ url: URL.createObjectURL(file), file })} + maxCount={4} + /> + + + + + ) + } + { + accessType === "2" + && ( + <> + + + + + + + + ({ url: URL.createObjectURL(file), file })} + maxCount={1} + /> + + + + + ) + } + + { + setNoticePopupVisible(false); + }} + > +
+

欢迎您到访秦皇岛港。为保障您的人身安全及港口生产作业秩序,请注意港口属于重点安全监管区域,存在大型机械作业、货物装卸、车辆往来等生产场景,可能面临机械伤害、物体打击、车辆碰撞等安全风险。请您认真阅读以下须知内容,确认遵守后签字:

+

1. 入港时请主动出示有效身份证件,配合安保人员进行身份核验与信息登记,凭港口核发的《临时访客证》入港,自觉接受出港查验;不转借、冒用访客凭证,不将无关人员带入港口。

+

2. 入港后请严格在指定区域活动,未经陪同人员及港口负责人许可,绝不擅自进入标有"禁止入内""危险区域"等标识的场所,不靠近起重机械、输送设备、危险品存储点等高危部位,不跨越安全护栏、警戒线,不在作业区域逗留围观。

+

3. 遵守港口生产秩序,不干扰装卸、运输、检修等正常工作;不随意触摸、操作生产设备、仪器仪表及安全设施,不移动、遮挡安全警示标识;如需拍摄港口场景,须提前征得港口方同意,不拍摄涉及安全、商业秘密的内容。

+

4. 严格遵守消防安全规定,不在港口内吸烟,不携带火种、易燃易爆物品、管制器具等违禁物品入港;发现火灾、设备故障等隐患或突发情况,第一时间告知陪同人员或港口工作人员,配合应急处置,不擅自行动引发次生风险。

+

5. 注意自身安全防护,行走时主动避让作业车辆与机械,不擅自横穿作业通道;雨天、雾天等恶劣天气下,听从陪同人员安排,加强安全防范。

+

6. 已知晓港口所去区域应急逃生路线,遇紧急情况按港口指引有序疏散。

+

本人确认已完整阅读并理解以上须知,承诺严格遵守。如因违反本须知及港口安全规定导致自身人身伤害或港口、他人财产损失,自愿承担全部责任。

+ { + setSignatureUrl(value.base64); + form.setFieldValue("signature", value.file); + }} + url={signatureUrl} + /> +
+ +
+
+
+
+ ); +} + +// 公共基础信息字段组件 +const BasicInfoFields = () => ( + <> + + + + + + + +); + +// 公共访问时间组件 +const VisitTime = ({ form }) => ( + <> + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => dayjs(value).format("YYYY-MM-DD")} + getValueProps={value => dayjs(value)} + > + + {value => value ? dayjs(value).format("YYYY-MM-DD") : "请选择访问起始时间"} + + + validatorEndTime(form.getFieldValue("visitStartTime")))()]} + trigger="onConfirm" + onClick={(_, pickerRef) => { + pickerRef.current?.open(); + }} + getValueFromEvent={value => dayjs(value).format("YYYY-MM-DD")} + getValueProps={value => dayjs(value)} + > + + {value => value ? dayjs(value).format("YYYY-MM-DD") : "请选择访问结束时间"} + + + +); + +// 公共地点字段组件 +const LocationFields = ({ primeportAreaList, doorType }) => ( + <> + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + ({ label: item.dictLabel, value: item.dictValue }))]} + > + {value => value?.[0]?.label || "请选择港区"} + + + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + + {value => value?.[0]?.label || "请选择一级口门"} + + + {doorType === "2" && } + +); + +// 封闭区域相关字段组件 +const EnclosedAreaFields = () => ( + <> + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + + {value => value?.[0]?.label || "请选择区域所属公司"} + + + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + + {value => value?.[0]?.label || "请选择封闭区域"} + + + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + + {value => value?.[0]?.label || "请选择口门"} + + + +); + +// 公共审批字段组件 +const Approval = ({ departmentList }) => ( + <> + { + pickerRef.current?.open(); + }} + > + + {value => value.length > 0 ? value.map(item => item?.label).filter(Boolean).join("-") : "请选择审批企业"} + + + { + pickerRef.current?.open(); + }} + getValueFromEvent={value => value[0]} + getValueProps={value => [value]} + > + + {value => value?.[0]?.label || "请选择审批人"} + + + +); + +// 安全须知和签名组件 +const SafetyNoticeAndSignature = ({ checkboxRef, signatureUrl, setNoticePopupVisible }) => ( + <> + + +
+ 我已阅读并同意 + { + checkboxRef.current.check(); + setNoticePopupVisible(true); + }} + > + 《安全进港须知》 + +
+
+
+ + + + + {signatureUrl && ( +
+ +
+ )} +
+ +); + +export default Connect([NS_GLOBAL], true)(Apply); diff --git a/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/ApplyList/index.js b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/ApplyList/index.js new file mode 100644 index 0000000..3fb3291 --- /dev/null +++ b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/ApplyList/index.js @@ -0,0 +1,127 @@ +import { DotLoading, InfiniteScroll } from "antd-mobile"; +import { useState } from "react"; +import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery"; + +function ApplyList() { + const query = useGetUrlQuery(); + + const [data, setData] = useState([]); + const [hasMore, setHasMore] = useState(true); + + const loadMore = async () => { + const data = [ + { name: "张三", phone: "12345678901", area: "A区", doorTypeName: "1号门", startTime: "2021-01-01 00:00:00", endTime: "2021-01-01 01:00:00", status: "通过", reason: "" }, + ]; + setData(val => [...val, ...data]); + setHasMore(data.length > 0); + }; + + return ( +
+
+ {data.map((item, index) => ( +
+ { + query.accessType === "2" && ( +
+
临时访客申请
+
+ 申请人: + {item.name} +
+
+ 手机号: + {item.phone} +
+
+ 申请区域: + {item.area} +
+
+ 申请口门: + {item.doorTypeName} +
+
+ 时间范围: + {item.startTime} + 至 + {item.endTime} +
+
+ 审核状态: + {item.status} +
+
+ 驳回原因: + {item.reason} +
+
+ ) + } + { + query.accessType === "1" && ( +
+
临时访客车辆申请
+
+ 申请人: + {item.name} +
+
+ 手机号: + {item.phone} +
+
+ 申请区域: + {item.area} +
+
+ 申请口门: + {item.doorTypeName} +
+
+ 时间范围: + {item.startTime} + 至 + {item.endTime} +
+
+ 车辆类型: + {item.carType} +
+
+ 车牌号: + {item.carNo} +
+
+ 审核状态: + {item.status} +
+
+ 驳回原因: + {item.reason} +
+
+ ) + } +
+ ))} +
+ + { + hasMore + ? ( + <> + Loading + + + ) + : ( + --- 我是有底线的 --- + ) + } + +
+ ); +} + +export default ApplyList; diff --git a/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/Success/index.js b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/Success/index.js new file mode 100644 index 0000000..f7fa7f1 --- /dev/null +++ b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/Success/index.js @@ -0,0 +1,23 @@ +import { QRCode } from "antd"; +import { CheckCircleFill } from "antd-mobile-icons"; +import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery"; + +function Success() { + const query = useGetUrlQuery(); + + return ( +
+
+ +
信息提交成功!
+
+
+ +
扫描识别二维码获取审批进度。
+
请长按或截屏保存二维码,关闭页面后将无法再获取二维码
+
+
+ ); +} + +export default Success; diff --git a/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/index.js b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/index.js new file mode 100644 index 0000000..bb74946 --- /dev/null +++ b/src/pages/Container/Mobile/firstLevelDoor/PersonnelApplication/index.js @@ -0,0 +1,5 @@ +function PersonnelApplication(props) { + return props.children; +} + +export default PersonnelApplication; diff --git a/src/pages/Container/Mobile/firstLevelDoor/index.js b/src/pages/Container/Mobile/firstLevelDoor/index.js new file mode 100644 index 0000000..906f44b --- /dev/null +++ b/src/pages/Container/Mobile/firstLevelDoor/index.js @@ -0,0 +1,5 @@ +function firstLevelDoor(props) { + return props.children; +} + +export default firstLevelDoor; diff --git a/src/pages/Container/Mobile/index.js b/src/pages/Container/Mobile/index.js new file mode 100644 index 0000000..4710e59 --- /dev/null +++ b/src/pages/Container/Mobile/index.js @@ -0,0 +1,5 @@ +function Mobile(props) { + return props.children; +} + +export default Mobile; diff --git a/src/pages/Container/Mobile/index.less b/src/pages/Container/Mobile/index.less new file mode 100644 index 0000000..5285831 --- /dev/null +++ b/src/pages/Container/Mobile/index.less @@ -0,0 +1,11 @@ +.adm-list, .adm-input { + --font-size: var(--adm-font-size-6); +} + +.adm-button { + font-size: var(--adm-font-size-6); +} + +.adm-list-item-content-prefix { + width: 130px; +} diff --git a/src/pages/Container/Stakeholder/PersonnelApplication/Add/index.js b/src/pages/Container/Stakeholder/PersonnelApplication/Add/index.js index ad1bb88..25b4521 100644 --- a/src/pages/Container/Stakeholder/PersonnelApplication/Add/index.js +++ b/src/pages/Container/Stakeholder/PersonnelApplication/Add/index.js @@ -14,6 +14,8 @@ function Add(props) { const [addPersonnelModalVisible, setAddPersonnelModalVisible] = useState(false); const [needToKnowModalVisible, setNeedToKnowModalVisible] = useState(false); + const signatureUrl = useRef(""); + const [form] = Form.useForm(); const onSubmit = async (values) => { @@ -137,12 +139,13 @@ function Add(props) { { needToKnowModalVisible && ( { setNeedToKnowModalVisible(false); }} onOk={(values) => { form.setFieldValue("todo8", values); + signatureUrl.current = values.base64; setNeedToKnowModalVisible(false); }} /> @@ -198,6 +201,7 @@ const AddPersonnelModalComponent = (props) => { const NeedToKnowModal = (props) => { const signatureUrl = useRef(""); + const signatureFile = useRef({}); return ( { message.warning("请签名"); return; } - props.onOk(signatureUrl.current); + props.onOk({ + base64: signatureUrl.current, + file: signatureFile.current, + }); }} >
@@ -227,6 +234,7 @@ const NeedToKnowModal = (props) => { { signatureUrl.current = value.base64; + signatureFile.current = value.file; }} url={props.signatureUrl} /> diff --git a/src/pages/Container/Stakeholder/VehicleApplication/Add/index.js b/src/pages/Container/Stakeholder/VehicleApplication/Add/index.js index 55bd437..04ad3da 100644 --- a/src/pages/Container/Stakeholder/VehicleApplication/Add/index.js +++ b/src/pages/Container/Stakeholder/VehicleApplication/Add/index.js @@ -14,6 +14,7 @@ import { NS_VEHICLE_APPLICATION } from "~/enumerate/namespace"; function Add(props) { const [needToKnowModalVisible, setNeedToKnowModalVisible] = useState(false); + const signatureUrl = useRef(""); const [form] = Form.useForm(); const todo7 = Form.useWatch("todo7", form); @@ -83,12 +84,13 @@ function Add(props) { { needToKnowModalVisible && ( { setNeedToKnowModalVisible(false); }} onOk={(values) => { - form.setFieldValue("todo8", values); + form.setFieldValue("todo8", values.file); + signatureUrl.current = values.base64; setNeedToKnowModalVisible(false); }} /> @@ -100,6 +102,7 @@ function Add(props) { const NeedToKnowModal = (props) => { const signatureUrl = useRef(""); + const signatureFile = useRef({}); return ( { message.warning("请签名"); return; } - props.onOk(signatureUrl.current); + props.onOk({ + base64: signatureUrl.current, + file: signatureFile.current, + }); }} >
@@ -129,6 +135,7 @@ const NeedToKnowModal = (props) => { { signatureUrl.current = value.base64; + signatureFile.current = value.file; }} url={props.signatureUrl} />