bug:16193、16192、16191、16190、16189、16179、16173、16168、16164、16160、16152、16150
parent
2ae282f3fe
commit
0b5bde42a8
|
|
@ -4,27 +4,22 @@ export const approvalUserList = declareRequest(
|
|||
"approvalUserLoading",
|
||||
`Post > @/primeport/mkmjApprovalUser/list`,
|
||||
);
|
||||
|
||||
export const approvalUserListAll = declareRequest(
|
||||
"approvalUserLoading",
|
||||
`Get > /primeport/mkmjApprovalUser/listAll`,
|
||||
);
|
||||
|
||||
export const approvalUserDelete = declareRequest(
|
||||
"approvalUserLoading",
|
||||
`Delete > @/primeport/mkmjApprovalUser/{id}`,
|
||||
);
|
||||
|
||||
export const approvalUserInfo = declareRequest(
|
||||
"approvalUserLoading",
|
||||
`Get > /primeport/mkmjApprovalUser/{id}`,
|
||||
);
|
||||
|
||||
export const approvalUserUpdate = declareRequest(
|
||||
"approvalUserLoading",
|
||||
`Put > @/primeport/mkmjApprovalUser/edit`,
|
||||
);
|
||||
|
||||
export const approvalUserAdd = declareRequest(
|
||||
"approvalUserLoading",
|
||||
`Post > @/primeport/mkmjApprovalUser/save`,
|
||||
|
|
|
|||
|
|
@ -20,10 +20,8 @@ export const enclosedAreaPersonnelApplySave = declareRequest(
|
|||
"enclosedAreaPersonnelApplyLoading",
|
||||
`Post > @/primeport/closedAreaPersonApply/save`,
|
||||
);
|
||||
export const enclosedAreaPersonnelApplyRecordsAccessRecordsList = declareRequest(
|
||||
"enclosedAreaPersonnelApplyLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const enclosedAreaPersonnelApplyRecordsAccessRecordsList
|
||||
= declareRequest("enclosedAreaPersonnelApplyLoading", `Post > @/primeport/`);
|
||||
export const xgfProjectListAll = declareRequest(
|
||||
"enclosedAreaPersonnelApplyLoading",
|
||||
`Get > /xgfManager/project/listAllPassedBySelfCorp`,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ export const enclosedEnterprisePersonnelPermissionsList = declareRequest(
|
|||
"enclosedEnterprisePersonnelPermissionsLoading",
|
||||
`Post > @/primeport/closedAreaPersonApply/getCorpUserList`,
|
||||
);
|
||||
export const enclosedEnterprisePersonnelPermissionsPersonnelRecordsList = declareRequest(
|
||||
"enclosedEnterprisePersonnelPermissionsLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const enclosedEnterprisePersonnelPermissionsPersonnelRecordsList
|
||||
= declareRequest(
|
||||
"enclosedEnterprisePersonnelPermissionsLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const enclosedEnterprisePersonnelPermissionsInfo = declareRequest(
|
||||
"enclosedEnterprisePersonnelPermissionsLoading",
|
||||
`Get > /primeport/closedAreaPersonApply/getAuthorizationPersonInfo/{id}`,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ export const enclosedPersonnelAndVehicleStatisticsList = declareRequest(
|
|||
"enclosedPersonnelAndVehicleStatisticsLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const enclosedPersonnelAndVehicleStatisticsVehicleEntryAndExitRecordsList = declareRequest(
|
||||
"enclosedPersonnelAndVehicleStatisticsLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const enclosedPersonnelAndVehicleStatisticsPersonnelEntryAndExitRecordsList = declareRequest(
|
||||
"enclosedPersonnelAndVehicleStatisticsLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const enclosedPersonnelAndVehicleStatisticsVehicleEntryAndExitRecordsList
|
||||
= declareRequest(
|
||||
"enclosedPersonnelAndVehicleStatisticsLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const enclosedPersonnelAndVehicleStatisticsPersonnelEntryAndExitRecordsList
|
||||
= declareRequest(
|
||||
"enclosedPersonnelAndVehicleStatisticsLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -52,20 +52,17 @@ export const firstLevelDoorInfoCameraInfo = declareRequest(
|
|||
"firstLevelDoorInfoCameraLoading",
|
||||
`Get > /primeport/video/{id}`,
|
||||
);
|
||||
export const firstLevelDoorInfoCameraGetRTSPUrl = declareRequest(
|
||||
"firstLevelDoorInfoCameraLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const firstLevelDoorInfoCameraGetPlayUrl = declareRequest(
|
||||
"firstLevelDoorInfoCameraLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const firstLevelDoorInfoCameraGetBatchPlayUrl = declareRequest(
|
||||
"firstLevelDoorInfoCameraLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const firstLevelDoorInfoFixedCameraList = declareRequest(
|
||||
`Get > /primeport/`,
|
||||
"firstLevelDoorInfoChannelLoading",
|
||||
`Post > @/videopatrol/fixedCamera/list`,
|
||||
);
|
||||
export const firstLevelDoorInfoFixedCameraInfo = declareRequest(
|
||||
"fixedCameraLoading",
|
||||
`Get > /videopatrol/fixedCamera/{id}`,
|
||||
);
|
||||
export const firstLevelDoorInfoFixedCameraGetPlayUrl = declareRequest(
|
||||
"fixedCameraLoading",
|
||||
`Get > /videopatrol/fixedCamera/getPlayUrl?indexCode={indexCode}`,
|
||||
);
|
||||
export const firstLevelDoorInfoChannelList = declareRequest(
|
||||
"firstLevelDoorInfoChannelLoading",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,5 @@ export const stockPersonnelAndVehiclesAuthorization = declareRequest(
|
|||
"stockPersonnelAndVehiclesLoading",
|
||||
`Post > @/primeport/personApply/authorization`,
|
||||
);
|
||||
export const stockPersonnelAndVehiclesVehicleManagementVehicleRecordsList = declareRequest(
|
||||
"stockPersonnelAndVehiclesLoading",
|
||||
`Post > @/primeport/`,
|
||||
);
|
||||
export const stockPersonnelAndVehiclesVehicleManagementVehicleRecordsList
|
||||
= declareRequest("stockPersonnelAndVehiclesLoading", `Post > @/primeport/`);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import BatchPlayPage from "~/pages/Container/Supervision/EnclosedArea/AreaAndEntrance/EnclosedAreaDoor/Camera/BatchPlay";
|
||||
|
||||
function BatchPlay(props) {
|
||||
return (<BatchPlayPage {...props} />);
|
||||
}
|
||||
|
||||
export default BatchPlay;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import BatchPlayPage from "~/pages/Container/Supervision/EnclosedArea/AreaAndEntrance/EnclosedAreaDoor/Channel/Camera/BatchPlay";
|
||||
|
||||
function BatchPlay(props) {
|
||||
return (<BatchPlayPage {...props} />);
|
||||
}
|
||||
|
||||
export default BatchPlay;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import CameraPage from "~/pages/Container/Supervision/EnclosedArea/AreaAndEntrance/EnclosedAreaDoor/Channel/Camera/List";
|
||||
|
||||
function Camera(props) {
|
||||
return (<CameraPage entrance="enterprise" {...props} />);
|
||||
}
|
||||
|
||||
export default Camera;
|
||||
|
|
@ -30,7 +30,11 @@ function List(props) {
|
|||
columns={[
|
||||
{ title: "姓名", dataIndex: "applyPersonUserName" },
|
||||
{ title: "手机号", dataIndex: "userPhone" },
|
||||
{ title: "身份证号", dataIndex: "userCard" },
|
||||
{
|
||||
title: "身份证号",
|
||||
dataIndex: "userCard",
|
||||
render: (_, record) => record.userCard ? atob(record.userCard) : "",
|
||||
},
|
||||
{ title: "申请区域", dataIndex: "closedAreaName" },
|
||||
{ title: "申请口门名称", dataIndex: "levelTwoMkmjName" },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function Review(props) {
|
|||
items={[
|
||||
{ label: "姓名", children: info.applyPersonUserName },
|
||||
{ label: "手机号", children: info.userPhone },
|
||||
{ label: "身份证号", children: info.userCard },
|
||||
{ label: "身份证号", children: info.userCard ? atob(info.userCard) : "" },
|
||||
{ label: "申请区域", children: info.closedAreaName },
|
||||
{ label: "一级口门", children: info.levelOneMkmjName },
|
||||
{ label: "二级口门", children: info.levelTwoMkmjName },
|
||||
|
|
|
|||
|
|
@ -34,7 +34,11 @@ function List(props) {
|
|||
columns={[
|
||||
{ title: "姓名", dataIndex: "applyPersonUserName" },
|
||||
{ title: "手机号", dataIndex: "userPhone" },
|
||||
{ title: "身份证号", dataIndex: "userCard" },
|
||||
{
|
||||
title: "身份证号",
|
||||
dataIndex: "userCard",
|
||||
render: (_, record) => record.userCard ? atob(record.userCard) : "",
|
||||
},
|
||||
{ title: "申请区域", dataIndex: "closedAreaName" },
|
||||
{ title: "申请口门名称", dataIndex: "levelTwoMkmjName" },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ function List(props) {
|
|||
{ title: "车辆类型", dataIndex: "vehicleTypeName" },
|
||||
{ title: "车牌号", dataIndex: "licenceNo" },
|
||||
{ title: "姓名", dataIndex: "applyPersonUserName" },
|
||||
{ title: "身份证号", dataIndex: "userCard" },
|
||||
{
|
||||
title: "身份证号",
|
||||
dataIndex: "userCard",
|
||||
render: (_, record) => record.userCard ? atob(record.userCard) : "",
|
||||
},
|
||||
{ title: "手机号", dataIndex: "userPhone" },
|
||||
{ title: "申请区域", dataIndex: "closedAreaName" },
|
||||
{ title: "口门名称", dataIndex: "levelTwoMkmjName" },
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function Review(props) {
|
|||
{ label: "车牌号", children: info.licenceNo },
|
||||
{ label: "驾驶人姓名", children: info.drivingUserName },
|
||||
{ label: "手机号", children: info.userPhone },
|
||||
{ label: "身份证号", children: info.userCard },
|
||||
{ label: "身份证号", children: info.userCard ? atob(info.userCard) : "" },
|
||||
{ label: "申请区域", children: info.closedAreaName },
|
||||
{ label: "一级口门名称", children: info.levelOneMkmjName },
|
||||
{ label: "二级口门名称", children: info.levelTwoMkmjName },
|
||||
|
|
|
|||
|
|
@ -35,7 +35,11 @@ function List(props) {
|
|||
{ title: "车辆类型", dataIndex: "vehicleTypeName" },
|
||||
{ title: "车牌号", dataIndex: "licenceNo" },
|
||||
{ title: "姓名", dataIndex: "applyPersonUserName" },
|
||||
{ title: "身份证号", dataIndex: "userCard" },
|
||||
{
|
||||
title: "身份证号",
|
||||
dataIndex: "userCard",
|
||||
render: (_, record) => record.userCard ? atob(record.userCard) : "",
|
||||
},
|
||||
{ title: "手机号", dataIndex: "userPhone" },
|
||||
{ title: "申请区域", dataIndex: "closedAreaName" },
|
||||
{ title: "口门名称", dataIndex: "levelTwoMkmjName" },
|
||||
|
|
|
|||
|
|
@ -82,8 +82,7 @@ function Apply(props) {
|
|||
const licensePlateTypeData = await getDictionary({ dictValue: "LICENSE_PLATE_TYPE" });
|
||||
setLicensePlateTypeList(licensePlateTypeData);
|
||||
const { data: departmentList } = await props["getDepartmentListTree"]({ enterpriseType: [2] });
|
||||
const transformedDepartmentList = transformTreeList(departmentList, "name", "id");
|
||||
setDepartmentList(transformedDepartmentList);
|
||||
setDepartmentList(departmentList);
|
||||
};
|
||||
|
||||
const getApprovalUserList = async (corpId) => {
|
||||
|
|
@ -187,6 +186,7 @@ function Apply(props) {
|
|||
informSignId,
|
||||
jurisdictionalCorpId: values.jurisdictionalCorpId?.at(-1),
|
||||
closedAreaId: values.closedAreaId?.at(-1),
|
||||
userCard: btoa(values.userCard),
|
||||
});
|
||||
if (success) {
|
||||
props.history.push(`./success?id=${data.id}&tmpApplyType=${values.tmpApplyType}&tmpMkmjType=${values.tmpMkmjType}`);
|
||||
|
|
@ -721,14 +721,21 @@ const EnclosedAreaFields = ({
|
|||
onClick={(_, pickerRef) => {
|
||||
pickerRef.current?.open();
|
||||
}}
|
||||
getValueFromEvent={value => value[0]}
|
||||
getValueProps={value => [value]}
|
||||
>
|
||||
<Cascader
|
||||
options={departmentList}
|
||||
<Picker
|
||||
columns={[departmentList.map(item => ({ label: item.name, value: item.id }))]}
|
||||
onConfirm={(value) => {
|
||||
form.setFieldValue("jurisdictionalCorpName", getTreeLabelName(departmentList, value.at(-1)));
|
||||
if (value.length > 0) {
|
||||
getEnclosedAreaList(value.at(-1));
|
||||
getApprovalUserList(value.at(-1));
|
||||
form.setFieldValue("jurisdictionalCorpName", getLabelName({
|
||||
list: departmentList,
|
||||
status: value[0],
|
||||
idKey: "id",
|
||||
nameKey: "name",
|
||||
}));
|
||||
if (value[0]) {
|
||||
getEnclosedAreaList(value[0]);
|
||||
getApprovalUserList(value[0]);
|
||||
}
|
||||
form.setFieldValue("closedAreaId", "");
|
||||
form.setFieldValue("closedAreaName", "");
|
||||
|
|
@ -736,8 +743,8 @@ const EnclosedAreaFields = ({
|
|||
form.setFieldValue("levelTwoMkmjName", "");
|
||||
}}
|
||||
>
|
||||
{value => value.length > 0 ? value.map(item => item?.label).filter(Boolean).join("-") : "请选择区域所属公司"}
|
||||
</Cascader>
|
||||
{value => value?.[0]?.label || "请选择区域所属公司"}
|
||||
</Picker>
|
||||
</Form.Item>
|
||||
<Form.Item name="jurisdictionalCorpName" label="区域所属公司名称" noStyle>
|
||||
<input type="hidden" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.adm-list, .adm-input {
|
||||
.adm-list,
|
||||
.adm-input {
|
||||
--font-size: var(--adm-font-size-6);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ function Add(props) {
|
|||
employeePersonUserName: item.userName,
|
||||
userFaceUrl: item.userFaceUrl,
|
||||
userPhone: item.phone,
|
||||
userCard: item.userCard,
|
||||
userCard: btoa(item.userCard),
|
||||
...item,
|
||||
})),
|
||||
informSignId,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { UPLOAD_FILE_TYPE_ENUM } from "zy-react-library/enum/uploadFile/gwj";
|
|||
import useGetFile from "zy-react-library/hooks/useGetFile";
|
||||
import useTable from "zy-react-library/hooks/useTable";
|
||||
import { getLabelName } from "zy-react-library/utils";
|
||||
import {ENCLOSED_AREA_AUDIT_STATUS_ENUM, TRAINING_STATE_ENUM} from "~/enumerate/constant";
|
||||
import { ENCLOSED_AREA_AUDIT_STATUS_ENUM } from "~/enumerate/constant";
|
||||
import { NS_PERSONNEL_APPLICATION } from "~/enumerate/namespace";
|
||||
|
||||
function List(props) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import PreviewImg from "zy-react-library/components/PreviewImg";
|
|||
import Search from "zy-react-library/components/Search";
|
||||
import Table from "zy-react-library/components/Table";
|
||||
import useTable from "zy-react-library/hooks/useTable";
|
||||
import { getLabelName } from "zy-react-library/utils";
|
||||
import { TRAINING_STATE_ENUM } from "~/enumerate/constant";
|
||||
import { NS_PERSONNEL_PERMISSION_RECORDS } from "~/enumerate/namespace";
|
||||
import {getLabelName} from "zy-react-library/utils";
|
||||
import {TRAINING_STATE_ENUM} from "~/enumerate/constant";
|
||||
|
||||
function List(props) {
|
||||
const [infoModalVisible, setInfoModalVisible] = useState(false);
|
||||
|
|
@ -35,7 +35,7 @@ function List(props) {
|
|||
columns={[
|
||||
{ title: "姓名", dataIndex: "employeePersonUserName" },
|
||||
{ title: "部门", dataIndex: "personDepartmentName" },
|
||||
{ title: "是否培训", dataIndex: "trainingState" ,render: (_, record) => getLabelName({ list: TRAINING_STATE_ENUM, status: record.trainingState })},
|
||||
{ title: "是否培训", dataIndex: "trainingState", render: (_, record) => getLabelName({ list: TRAINING_STATE_ENUM, status: record.trainingState }) },
|
||||
{ title: "涉及项目", dataIndex: "projectName" },
|
||||
// { title: "口门权限范围", dataIndex: "todo5" },
|
||||
{
|
||||
|
|
@ -108,7 +108,7 @@ const InfoModalComponent = (props) => {
|
|||
items={[
|
||||
{ label: "姓名", children: info.applyPersonUserName },
|
||||
{ label: "手机号", children: info.userPhone },
|
||||
{ label: "身份证号", children: info.userCard },
|
||||
{ label: "身份证号", children: info.userCard ? atob(info.userCard) : "" },
|
||||
{ label: "访问起始时间", children: info.visitStartTime },
|
||||
{ label: "访问结束时间", children: info.visitEndTime },
|
||||
{ label: "口门权限范围", children: info.todo6 },
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ function Add(props) {
|
|||
message.warning("请勾选《安全进港须知》并签字");
|
||||
return;
|
||||
}
|
||||
if (values.drivingLicenseFile.length !== 2) {
|
||||
message.error("请上传两张驾驶证");
|
||||
return;
|
||||
}
|
||||
const { id: informSignId } = await uploadFile({
|
||||
files: [{ originFileObj: values.informSignFile }],
|
||||
single: false,
|
||||
|
|
@ -152,7 +156,12 @@ function Add(props) {
|
|||
{ name: "auditCorpName", label: "审核企业名称", onlyForLabel: true },
|
||||
{ name: "auditDeptId", label: "审核部门ID", onlyForLabel: true },
|
||||
{ name: "auditDeptName", label: "审核部门名称", onlyForLabel: true },
|
||||
{ name: "visitTime", label: "时间", render: FORM_ITEM_RENDER_ENUM.DATE_RANGE, componentProps: { disabled: true } },
|
||||
{
|
||||
name: "visitTime",
|
||||
label: "时间",
|
||||
render: FORM_ITEM_RENDER_ENUM.DATE_RANGE,
|
||||
componentProps: { disabled: true },
|
||||
},
|
||||
{
|
||||
name: "gateLevelAuthArea",
|
||||
label: "访问港区",
|
||||
|
|
@ -250,8 +259,43 @@ function Add(props) {
|
|||
],
|
||||
formItemProps: { validateTrigger: ["onChange", "onBlur"] },
|
||||
},
|
||||
{ name: "attachmentFile", label: "车辆照片", span: 24, render: (<Upload />) },
|
||||
{ name: "drivingLicenseFile", label: "行驶证照片", span: 24, render: (<Upload />) },
|
||||
{
|
||||
name: "attachmentFile",
|
||||
label: "车辆照片",
|
||||
span: 24,
|
||||
render: (
|
||||
<Upload
|
||||
maxCount={4}
|
||||
size={5}
|
||||
tipContent={(
|
||||
<div style={{ color: "red", fontSize: 12 }}>
|
||||
<div>1.上限4张</div>
|
||||
<div>2. 请从车辆左前侧45°拍摄,车牌清晰可见</div>
|
||||
<div>3. 背景简洁,避免逆光或阴影</div>
|
||||
<div>4. 支持格式:.jpg/.jpeg/.png,单张5MB</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "drivingLicenseFile",
|
||||
label: "行驶证照片",
|
||||
span: 24,
|
||||
render: (
|
||||
<Upload
|
||||
maxCount={2}
|
||||
size={5}
|
||||
tipContent={(
|
||||
<div style={{ color: "red", fontSize: 12 }}>
|
||||
<div>1. 请拍摄行驶证正面和反面,确保四角完整、无遮挡</div>
|
||||
<div>2. 文字、印章清晰可见,避免反光</div>
|
||||
<div>3. 支持格式:.jpg/.jpeg/.png,单张5MB</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "securityProtocol",
|
||||
label: " ",
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ function Add(props) {
|
|||
render: (
|
||||
<DepartmentSelectTree
|
||||
searchType="inType"
|
||||
level={1}
|
||||
params={{ enterpriseType: [2] }}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,12 @@ function Add(props) {
|
|||
message.warning("请勾选《安全进港须知》并签字");
|
||||
return;
|
||||
}
|
||||
if (isSelectVehicle === 2 || props.entrance === "stakeholder") {
|
||||
if (values.drivingLicenseFile.length !== 2) {
|
||||
message.error("请上传两张驾驶证");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let carBelongType = 1;
|
||||
if (props.entrance === "enterprise")
|
||||
carBelongType = 2;
|
||||
|
|
@ -186,6 +192,7 @@ function Add(props) {
|
|||
render: (
|
||||
<DepartmentSelectTree
|
||||
searchType="inType"
|
||||
level={1}
|
||||
params={{ enterpriseType: [2] }}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
|
|
@ -384,8 +391,43 @@ function Add(props) {
|
|||
],
|
||||
formItemProps: { validateTrigger: ["onChange", "onBlur"] },
|
||||
},
|
||||
{ name: "attachmentFile", label: "车辆照片", span: 24, render: (<Upload />) },
|
||||
{ name: "drivingLicenseFile", label: "行驶证照片", span: 24, render: (<Upload />) },
|
||||
{
|
||||
name: "attachmentFile",
|
||||
label: "车辆照片",
|
||||
span: 24,
|
||||
render: (
|
||||
<Upload
|
||||
maxCount={4}
|
||||
size={5}
|
||||
tipContent={(
|
||||
<div style={{ color: "red", fontSize: 12 }}>
|
||||
<div>1.上限4张</div>
|
||||
<div>2. 请从车辆左前侧45°拍摄,车牌清晰可见</div>
|
||||
<div>3. 背景简洁,避免逆光或阴影</div>
|
||||
<div>4. 支持格式:.jpg/.jpeg/.png,单张5MB</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "drivingLicenseFile",
|
||||
label: "行驶证照片",
|
||||
span: 24,
|
||||
render: (
|
||||
<Upload
|
||||
maxCount={2}
|
||||
size={5}
|
||||
tipContent={(
|
||||
<div style={{ color: "red", fontSize: 12 }}>
|
||||
<div>1. 请拍摄行驶证正面和反面,确保四角完整、无遮挡</div>
|
||||
<div>2. 文字、印章清晰可见,避免反光</div>
|
||||
<div>3. 支持格式:.jpg/.jpeg/.png,单张5MB</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ name: "applyReason", label: "申请原因", span: 24, render: FORM_ITEM_RENDER_ENUM.TEXTAREA },
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function List(props) {
|
|||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 80,
|
||||
width: 150,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
|
|
@ -83,7 +83,7 @@ function List(props) {
|
|||
查看
|
||||
</Button>
|
||||
)}
|
||||
{props.permission(props.rejectReasonBtn || "jgd-enclosed-vehicle-records-bh") && (
|
||||
{record.auditFlag === 3 && props.permission(props.rejectReasonBtn || "jgd-enclosed-vehicle-records-bh") && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import BatchPlayPage from "~/pages/Container/Supervision/FirstLevelDoor/BasicInfo/FirstLevelDoorInfo/Camera/BatchPlay";
|
||||
|
||||
function BatchPlay(props) {
|
||||
return (<BatchPlayPage {...props} />);
|
||||
}
|
||||
|
||||
export default BatchPlay;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import BatchPlayPage from "../../../Camera/BatchPlay";
|
||||
|
||||
function BatchPlay(props) {
|
||||
return (<BatchPlayPage {...props} />);
|
||||
}
|
||||
|
||||
export default BatchPlay;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import CameraPage from "../../../Camera/List";
|
||||
|
||||
function Camera(props) {
|
||||
return (<CameraPage {...props} />);
|
||||
}
|
||||
|
||||
export default Camera;
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import CameraList from "../../Camera/List";
|
||||
|
||||
function Camera() {
|
||||
return (<CameraList />);
|
||||
function Camera(props) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
export default Camera;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,682 @@
|
|||
import LeftOutlined from "@ant-design/icons/LeftOutlined";
|
||||
import ReloadOutlined from "@ant-design/icons/ReloadOutlined";
|
||||
import RightOutlined from "@ant-design/icons/RightOutlined";
|
||||
import SearchOutlined from "@ant-design/icons/SearchOutlined";
|
||||
import VideoCameraOutlined from "@ant-design/icons/VideoCameraOutlined";
|
||||
import { Button, Card, Empty, Form, Input, message, Radio, Select, Tree } from "antd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Page from "zy-react-library/components/Page";
|
||||
import Video from "zy-react-library/components/Video";
|
||||
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
|
||||
import { buildPlayableVideoSource } from "~/utils/video";
|
||||
|
||||
const IS_ONLINE_ENUM = [
|
||||
{ name: "否", bianma: "0" },
|
||||
{ name: "是", bianma: "1" },
|
||||
];
|
||||
// 巡屏页:
|
||||
// 1. 左侧树控制参与巡屏的视频范围
|
||||
// 2. 右侧按宫格展示视频,并支持自动轮巡
|
||||
// 3. 每个视频卡片都支持截图并进入隐患上报弹窗
|
||||
const LAYOUT_MODES = [
|
||||
{ label: "4 宫格", value: 4 },
|
||||
{ label: "6 宫格", value: 6 },
|
||||
{ label: "9 宫格", value: 9 },
|
||||
{ label: "单画面", value: 1 },
|
||||
];
|
||||
const PLAY_READY_TIMEOUT = 25000;
|
||||
const PLAY_RETRY_INTERVAL = 5000;
|
||||
|
||||
function BatchPlay() {
|
||||
const [form] = Form.useForm();
|
||||
// 宫格模式、勾选状态、轮巡间隔、当前分组共同决定右侧展示内容。
|
||||
const [layoutMode, setLayoutMode] = useState(6);
|
||||
const [checkedKeys, setCheckedKeys] = useState([]);
|
||||
const [currentGroup, setCurrentGroup] = useState(0);
|
||||
// 列表、播放地址、截图弹窗等状态都在页面内独立管理。
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [videoList, setVideoList] = useState([]);
|
||||
const [filteredVideoList, setFilteredVideoList] = useState([]);
|
||||
const [playUrls, setPlayUrls] = useState({});
|
||||
const [playStatusMap, setPlayStatusMap] = useState({});
|
||||
const videoCardRefs = useRef({});
|
||||
const playRetryTimersRef = useRef({});
|
||||
const playFailTimersRef = useRef({});
|
||||
const playTaskIdsRef = useRef({});
|
||||
const playRequestingRef = useRef({});
|
||||
|
||||
const query = useGetUrlQuery();
|
||||
|
||||
// 巡屏页批量取播放地址时直接走 fetch,避免全局 loading 干扰页面体验。
|
||||
const requestPlayUrlSilently = async (indexCode) => {
|
||||
const apiHost = window.__JJB_ENVIRONMENT__?.API_HOST || "";
|
||||
const token = window.sessionStorage?.token;
|
||||
const tenantCode = window.__JJB_ENVIRONMENT__?.tenantCode;
|
||||
const queryString = new URLSearchParams({ indexCode }).toString();
|
||||
const response = await fetch(`${apiHost}/videopatrol/fixedCamera/getPlayUrl?${queryString}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...(token ? { token } : {}),
|
||||
...(tenantCode ? { tenantCode } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP_${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result?.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildPlayableVideoSource(result.data?.url || result.data || null).source || null;
|
||||
};
|
||||
|
||||
const clearVideoPlayTimers = (videoId) => {
|
||||
if (playRetryTimersRef.current[videoId]) {
|
||||
window.clearInterval(playRetryTimersRef.current[videoId]);
|
||||
delete playRetryTimersRef.current[videoId];
|
||||
}
|
||||
if (playFailTimersRef.current[videoId]) {
|
||||
window.clearTimeout(playFailTimersRef.current[videoId]);
|
||||
delete playFailTimersRef.current[videoId];
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllVideoPlayTimers = () => {
|
||||
Object.keys(playRetryTimersRef.current).forEach(clearVideoPlayTimers);
|
||||
Object.keys(playFailTimersRef.current).forEach(clearVideoPlayTimers);
|
||||
};
|
||||
|
||||
const isVideoReady = (videoId) => {
|
||||
const videoEl = videoCardRefs.current[videoId]?.querySelector("video");
|
||||
return !!videoEl && videoEl.readyState >= 2;
|
||||
};
|
||||
|
||||
const startVideoRetryMonitor = (video) => {
|
||||
const videoId = video?.id?.toString();
|
||||
if (!videoId || !video.cameraNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearVideoPlayTimers(videoId);
|
||||
playTaskIdsRef.current[videoId] = (playTaskIdsRef.current[videoId] || 0) + 1;
|
||||
const currentTaskId = playTaskIdsRef.current[videoId];
|
||||
playRequestingRef.current[videoId] = false;
|
||||
|
||||
const requestLatestPlayUrl = async () => {
|
||||
if (playRequestingRef.current[videoId]) {
|
||||
return false;
|
||||
}
|
||||
playRequestingRef.current[videoId] = true;
|
||||
try {
|
||||
const playUrl = await requestPlayUrlSilently(video.cameraNumber);
|
||||
if (playTaskIdsRef.current[videoId] !== currentTaskId) {
|
||||
return false;
|
||||
}
|
||||
if (playUrl) {
|
||||
setPlayUrls(prev => ({ ...prev, [videoId]: playUrl }));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
if (playTaskIdsRef.current[videoId] === currentTaskId) {
|
||||
playRequestingRef.current[videoId] = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const attemptPlayback = async () => {
|
||||
if (playTaskIdsRef.current[videoId] !== currentTaskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVideoReady(videoId)) {
|
||||
clearVideoPlayTimers(videoId);
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "success" }));
|
||||
return;
|
||||
}
|
||||
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "loading" }));
|
||||
await requestLatestPlayUrl();
|
||||
|
||||
if (playTaskIdsRef.current[videoId] !== currentTaskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVideoReady(videoId)) {
|
||||
clearVideoPlayTimers(videoId);
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "success" }));
|
||||
}
|
||||
};
|
||||
|
||||
// 巡屏页和固定摄像头查看保持同样节奏:当前宫格中的卡片每 5 秒重新取一次地址,25 秒才判失败。
|
||||
playRetryTimersRef.current[videoId] = window.setInterval(() => {
|
||||
attemptPlayback();
|
||||
}, PLAY_RETRY_INTERVAL);
|
||||
|
||||
playFailTimersRef.current[videoId] = window.setTimeout(() => {
|
||||
if (playTaskIdsRef.current[videoId] !== currentTaskId || isVideoReady(videoId)) {
|
||||
clearVideoPlayTimers(videoId);
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "success" }));
|
||||
return;
|
||||
}
|
||||
|
||||
clearVideoPlayTimers(videoId);
|
||||
setPlayUrls((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[videoId];
|
||||
return next;
|
||||
});
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "error" }));
|
||||
}, PLAY_READY_TIMEOUT);
|
||||
};
|
||||
|
||||
// 前端筛选只控制当前巡屏页展示,不会影响数据库数据。
|
||||
const filterVideos = (list, values = {}) => {
|
||||
const { videoName, isOnline } = values;
|
||||
return list.filter((video) => {
|
||||
const matchName = !videoName || video.videoName?.includes(videoName);
|
||||
const matchStatus = isOnline === undefined || isOnline === "" || video.isOnline?.toString() === isOnline;
|
||||
return matchName && matchStatus;
|
||||
});
|
||||
};
|
||||
|
||||
// 加载视频台账列表,并默认让当前筛选结果全部参与巡屏。
|
||||
// 巡屏页左侧树与视频台账列表保持同一取数口径,避免绕开台账权限过滤。
|
||||
const loadVideoList = async () => {
|
||||
const list = query.videoResourceData ? JSON.parse(query.videoResourceData) : [];
|
||||
const filteredList = filterVideos(list, form.getFieldsValue());
|
||||
const nextCheckedKeys = filteredList.map(item => item.id?.toString()).filter(Boolean);
|
||||
setVideoList(list);
|
||||
setFilteredVideoList(filteredList);
|
||||
setLoadFailed(false);
|
||||
setCheckedKeys(nextCheckedKeys);
|
||||
setCurrentGroup(0);
|
||||
};
|
||||
|
||||
// 页面首次进入时拉一次视频列表。
|
||||
useEffect(() => {
|
||||
loadVideoList();
|
||||
}, []);
|
||||
|
||||
// 页面卸载时清理自动轮巡定时器。
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearAllVideoPlayTimers();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 左侧勾选决定参与巡屏的视频集合,右侧只展示当前分组的视频。
|
||||
const selectedVideos = videoList.filter(item => checkedKeys.includes(item.id?.toString()));
|
||||
const maxGroup = Math.max(Math.ceil(selectedVideos.length / layoutMode) - 1, 0);
|
||||
const displayVideos = selectedVideos.slice(
|
||||
currentGroup * layoutMode,
|
||||
currentGroup * layoutMode + layoutMode,
|
||||
);
|
||||
|
||||
// 宫格切换或筛选变化后,如果当前页码越界,自动回到第一页。
|
||||
useEffect(() => {
|
||||
if (currentGroup > maxGroup) {
|
||||
setCurrentGroup(0);
|
||||
}
|
||||
}, [currentGroup, maxGroup]);
|
||||
|
||||
// 勾选集变化后,静默批量向海康拉取播放地址;
|
||||
// 巡屏页不再回退数据库中的原始视频地址。
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
const fetchPlayUrls = async () => {
|
||||
if (checkedKeys.length === 0) {
|
||||
clearAllVideoPlayTimers();
|
||||
setPlayUrls({});
|
||||
setPlayStatusMap({});
|
||||
return;
|
||||
}
|
||||
|
||||
const nextPlayUrls = {};
|
||||
const nextPlayStatusMap = {};
|
||||
const videosNeedFetch = [];
|
||||
for (const video of selectedVideos) {
|
||||
const videoId = video.id?.toString();
|
||||
if (video.cameraNumber) {
|
||||
nextPlayStatusMap[videoId] = "loading";
|
||||
videosNeedFetch.push(video);
|
||||
}
|
||||
else {
|
||||
nextPlayStatusMap[videoId] = "error";
|
||||
}
|
||||
}
|
||||
|
||||
if (!canceled) {
|
||||
setPlayUrls(nextPlayUrls);
|
||||
setPlayStatusMap(nextPlayStatusMap);
|
||||
}
|
||||
|
||||
if (videosNeedFetch.length > 0) {
|
||||
const hideLoading = message.loading(`正在获取 ${videosNeedFetch.length} 个视频的播放地址...`, 0);
|
||||
let failedCount = 0;
|
||||
for (const video of videosNeedFetch) {
|
||||
const videoId = video.id?.toString();
|
||||
try {
|
||||
const playUrl = await requestPlayUrlSilently(video.cameraNumber);
|
||||
if (playUrl) {
|
||||
nextPlayUrls[videoId] = playUrl;
|
||||
// 仅拿到播放地址不代表已经真正出画面;
|
||||
// 截图按钮仍需等视频元素 ready 后,再由重试监控把状态切到 success。
|
||||
nextPlayStatusMap[videoId] = "loading";
|
||||
}
|
||||
else {
|
||||
failedCount += 1;
|
||||
nextPlayStatusMap[videoId] = "error";
|
||||
}
|
||||
}
|
||||
catch {
|
||||
failedCount += 1;
|
||||
nextPlayStatusMap[videoId] = "error";
|
||||
}
|
||||
}
|
||||
hideLoading();
|
||||
if (!canceled) {
|
||||
setPlayUrls({ ...nextPlayUrls });
|
||||
setPlayStatusMap({ ...nextPlayStatusMap });
|
||||
if (failedCount > 0) {
|
||||
message.warning(`有 ${failedCount} 个视频播放地址获取失败`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchPlayUrls();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [checkedKeys, videoList]);
|
||||
|
||||
const displayVideoKey = displayVideos.map(video => `${video?.id ?? "empty"}:${video?.cameraNumber ?? ""}`).join("|");
|
||||
|
||||
useEffect(() => {
|
||||
const displayVideoIds = new Set(displayVideos.map(video => video?.id?.toString()).filter(Boolean));
|
||||
|
||||
Object.keys(playRetryTimersRef.current).forEach((videoId) => {
|
||||
if (!displayVideoIds.has(videoId)) {
|
||||
clearVideoPlayTimers(videoId);
|
||||
}
|
||||
});
|
||||
|
||||
displayVideos.forEach((video) => {
|
||||
if (video?.cameraNumber) {
|
||||
startVideoRetryMonitor(video);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
displayVideos.forEach((video) => {
|
||||
const videoId = video?.id?.toString();
|
||||
if (videoId) {
|
||||
clearVideoPlayTimers(videoId);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [displayVideoKey]);
|
||||
|
||||
// 手动切换上一组/下一组。
|
||||
const handlePrevGroup = () => {
|
||||
if (selectedVideos.length === 0)
|
||||
return;
|
||||
setCurrentGroup(prev => (prev <= 0 ? maxGroup : prev - 1));
|
||||
};
|
||||
|
||||
const handleNextGroup = () => {
|
||||
if (selectedVideos.length === 0)
|
||||
return;
|
||||
setCurrentGroup(prev => (prev >= maxGroup ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
// 单个卡片播放失败时支持手动重试获取播放地址。
|
||||
const retryPlayUrl = async (video) => {
|
||||
const videoId = video.id?.toString();
|
||||
if (!videoId)
|
||||
return;
|
||||
|
||||
if (!video.cameraNumber) {
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "error" }));
|
||||
message.warning("当前视频缺少摄像头编号,无法获取播放地址");
|
||||
return;
|
||||
}
|
||||
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "loading" }));
|
||||
try {
|
||||
const playUrl = await requestPlayUrlSilently(video.cameraNumber);
|
||||
if (!playUrl) {
|
||||
throw new Error("empty play url");
|
||||
}
|
||||
setPlayUrls(prev => ({ ...prev, [videoId]: playUrl }));
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "loading" }));
|
||||
startVideoRetryMonitor(video);
|
||||
}
|
||||
catch {
|
||||
setPlayUrls((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[videoId];
|
||||
return next;
|
||||
});
|
||||
setPlayStatusMap(prev => ({ ...prev, [videoId]: "loading" }));
|
||||
startVideoRetryMonitor(video);
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索后只保留命中的视频,并默认全部勾选参与巡屏。
|
||||
const handleSearch = (values) => {
|
||||
const filtered = filterVideos(videoList, values);
|
||||
setFilteredVideoList(filtered);
|
||||
setCheckedKeys(filtered.map(item => item.id?.toString()).filter(Boolean));
|
||||
setCurrentGroup(0);
|
||||
};
|
||||
|
||||
// 重置后恢复完整列表、全选状态,并按当前宫格重新计算是否自动轮巡。
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
setFilteredVideoList(videoList);
|
||||
setCheckedKeys(videoList.map(item => item.id?.toString()).filter(Boolean));
|
||||
setCurrentGroup(0);
|
||||
};
|
||||
|
||||
// 左侧树节点的在线状态样式与右侧是否参与巡屏同时展示在标题里。
|
||||
const renderOnlineStatus = (status) => {
|
||||
const statusValue = `${status ?? ""}`;
|
||||
const isOnline = statusValue === "1";
|
||||
const text = isOnline ? "在线" : "离线";
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
padding: "0 6px",
|
||||
borderRadius: 10,
|
||||
fontSize: 12,
|
||||
lineHeight: "20px",
|
||||
color: isOnline ? "#389e0d" : "#595959",
|
||||
background: isOnline ? "#f6ffed" : "#fafafa",
|
||||
border: `1px solid ${isOnline ? "#b7eb8f" : "#d9d9d9"}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: isOnline ? "#52c41a" : "#8c8c8c",
|
||||
}}
|
||||
/>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// 左侧树只渲染当前筛选结果,颜色用于区分是否参与右侧轮巡。
|
||||
const treeData = filteredVideoList.map(video => ({
|
||||
title: (
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, width: "100%" }}>
|
||||
<span
|
||||
style={{
|
||||
color: checkedKeys.includes(video.id?.toString()) ? "#1677ff" : "#000",
|
||||
fontWeight: checkedKeys.includes(video.id?.toString()) ? 600 : 400,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{video.videoName}
|
||||
</span>
|
||||
{renderOnlineStatus(video.isOnline)}
|
||||
</div>
|
||||
),
|
||||
key: video.id?.toString(),
|
||||
}));
|
||||
|
||||
// 不同宫格对应不同的网格布局。
|
||||
const getGridColumns = () => {
|
||||
if (layoutMode === 1)
|
||||
return "1fr";
|
||||
if (layoutMode === 4)
|
||||
return "repeat(2, 1fr)";
|
||||
return "repeat(3, 1fr)";
|
||||
};
|
||||
|
||||
const getGridRows = () => {
|
||||
if (layoutMode === 1)
|
||||
return "1fr";
|
||||
if (layoutMode === 4)
|
||||
return "repeat(2, 1fr)";
|
||||
if (layoutMode === 9)
|
||||
return "repeat(3, 1fr)";
|
||||
return "repeat(2, 1fr)";
|
||||
};
|
||||
|
||||
// 渲染单个视频卡片:包含播放器、失败态、截图按钮和视频名称。
|
||||
const renderVideoCard = (video, index) => {
|
||||
const playUrl = video ? playUrls[video.id?.toString()] : null;
|
||||
const playStatus = video ? playStatusMap[video.id?.toString()] : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={video?.id || index}
|
||||
ref={(node) => {
|
||||
if (video?.id) {
|
||||
videoCardRefs.current[video.id] = node;
|
||||
}
|
||||
}}
|
||||
style={{ display: "flex", flexDirection: "column", height: "100%", minHeight: 0, overflow: "hidden" }}
|
||||
>
|
||||
<div style={{ flex: 1, minHeight: 0, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<Card
|
||||
size="small"
|
||||
bodyStyle={{
|
||||
padding: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#000",
|
||||
}}
|
||||
style={{ width: "100%", height: "100%", border: "1px solid #d9d9d9", borderRadius: 4, overflow: "hidden" }}
|
||||
>
|
||||
<div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "#000" }}>
|
||||
{video
|
||||
? (
|
||||
playStatus === "error"
|
||||
? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "#ff4d4f",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<VideoCameraOutlined style={{ fontSize: 32 }} />
|
||||
<span style={{ fontSize: 12 }}>视频加载失败</span>
|
||||
<Button size="small" onClick={() => retryPlayUrl(video)}>重试</Button>
|
||||
</div>
|
||||
)
|
||||
: playUrl
|
||||
? (
|
||||
<div style={{ width: "100%", height: "100%", background: "#000" }}>
|
||||
<Video
|
||||
inline
|
||||
height="100%"
|
||||
width="100%"
|
||||
source={playUrl}
|
||||
isLive
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "#666",
|
||||
}}
|
||||
>
|
||||
<VideoCameraOutlined style={{ fontSize: 32, marginBottom: 8 }} />
|
||||
<span style={{ fontSize: 12 }}>加载中...</span>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无视频" />}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: 24,
|
||||
lineHeight: "24px",
|
||||
textAlign: "center",
|
||||
fontSize: 12,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{video ? video.videoName : "-"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page headerTitle="巡屏监控" isShowFooter={false} style={{ padding: "4px 16px", overflow: "hidden" }}>
|
||||
<div style={{ display: "flex", gap: 8, height: "calc(100vh - 100px)", overflow: "hidden" }}>
|
||||
{/* 左侧视频树:控制哪些视频参与右侧巡屏。 */}
|
||||
<Card
|
||||
title="视频列表"
|
||||
size="small"
|
||||
style={{ width: 280, minWidth: 280, maxWidth: 280, height: "100%" }}
|
||||
bodyStyle={{ height: "calc(100% - 56px)", overflow: "auto", padding: 8 }}
|
||||
>
|
||||
{filteredVideoList.length > 0
|
||||
? (
|
||||
<Tree
|
||||
checkable
|
||||
blockNode
|
||||
checkedKeys={checkedKeys}
|
||||
onCheck={(keys) => {
|
||||
const nextKeys = (Array.isArray(keys) ? keys : keys.checked).map(String);
|
||||
setCheckedKeys(nextKeys);
|
||||
setCurrentGroup(0);
|
||||
}}
|
||||
treeData={treeData}
|
||||
style={{ background: "#fafafa", padding: 8, borderRadius: 4 }}
|
||||
/>
|
||||
)
|
||||
: <Empty description={loadFailed ? "视频加载失败,请重试" : "暂无视频"} />}
|
||||
</Card>
|
||||
|
||||
{/* 右侧巡屏主区域:包含搜索、宫格切换、视频网格和底部轮巡控制。 */}
|
||||
<Card
|
||||
style={{ flex: 1, height: "100%" }}
|
||||
bodyStyle={{ height: "100%", padding: 12, display: "flex", flexDirection: "column", overflow: "hidden" }}
|
||||
>
|
||||
{/* 搜索区:按视频名称和在线状态筛选左树与右侧轮巡集合。 */}
|
||||
<Form form={form} onFinish={handleSearch} style={{ display: "flex", gap: 16, marginBottom: 16, flexShrink: 0 }}>
|
||||
<Form.Item name="videoName" label="视频名称" style={{ marginBottom: 0 }}>
|
||||
<Input placeholder="请输入视频名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="isOnline" label="当前状态" style={{ marginBottom: 0 }}>
|
||||
<Select
|
||||
placeholder="请选择状态"
|
||||
options={IS_ONLINE_ENUM}
|
||||
fieldNames={{ label: "name", value: "bianma" }}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>搜索</Button>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button onClick={handleReset} icon={<ReloadOutlined />}>重置</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* 宫格切换区:修改宫格后会重新计算当前分组和自动轮巡状态。 */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16, marginTop: 10, flexShrink: 0 }}>
|
||||
<Radio.Group
|
||||
value={layoutMode}
|
||||
onChange={(e) => {
|
||||
const nextLayoutMode = e.target.value;
|
||||
setLayoutMode(nextLayoutMode);
|
||||
setCurrentGroup(0);
|
||||
}}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
>
|
||||
{LAYOUT_MODES.map(mode => (
|
||||
<Radio.Button key={mode.value} value={mode.value}>
|
||||
{mode.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
{/* 视频网格区:按当前分组渲染参与轮巡的视频。 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
gridTemplateColumns: getGridColumns(),
|
||||
gridTemplateRows: getGridRows(),
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: layoutMode }).map((_, index) => renderVideoCard(displayVideos[index], index))}
|
||||
</div>
|
||||
|
||||
{/* 底部控制区:调整轮巡间隔、开始/停止轮巡、上一组/下一组。 */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
padding: "8px 0",
|
||||
flexShrink: 0,
|
||||
borderTop: "1px solid #f0f0f0",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Button icon={<LeftOutlined />} onClick={handlePrevGroup} disabled={selectedVideos.length === 0}>上一组</Button>
|
||||
<Button icon={<RightOutlined />} onClick={handleNextGroup} disabled={selectedVideos.length === 0}>下一组</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default BatchPlay;
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
import VideoCameraOutlined from "@ant-design/icons/VideoCameraOutlined";
|
||||
import { Button, Modal, Spin } from "antd";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Video from "zy-react-library/components/Video";
|
||||
import { buildPlayableVideoSource, getVideoPlaybackFailureText } from "~/utils/video";
|
||||
|
||||
const PLAY_READY_TIMEOUT = 25000;
|
||||
const PLAY_RETRY_INTERVAL = 5000;
|
||||
const PLAY_SOURCE_TYPE = {
|
||||
HIKVISION: "hikvision",
|
||||
DATABASE: "database",
|
||||
};
|
||||
|
||||
const VideoPlayModal = (props) => {
|
||||
const {
|
||||
onClose,
|
||||
videoName,
|
||||
videoAddress,
|
||||
cameraNumber,
|
||||
getPlayUrl,
|
||||
playSourceType = PLAY_SOURCE_TYPE.HIKVISION,
|
||||
} = props;
|
||||
const [playUrl, setPlayUrl] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [warningText, setWarningText] = useState("");
|
||||
const [originalPlayUrl, setOriginalPlayUrl] = useState("");
|
||||
const [playerKey, setPlayerKey] = useState(0);
|
||||
const getPlayUrlRef = useRef(getPlayUrl);
|
||||
const playerContainerRef = useRef(null);
|
||||
const retryIntervalRef = useRef(null);
|
||||
const failTimerRef = useRef(null);
|
||||
const requestRunningRef = useRef(false);
|
||||
const requestTaskIdRef = useRef(0);
|
||||
const playUrlRef = useRef("");
|
||||
const originalPlayUrlRef = useRef("");
|
||||
const upgradedToHttpsRef = useRef(false);
|
||||
|
||||
const clearPlayTimers = () => {
|
||||
if (retryIntervalRef.current) {
|
||||
window.clearInterval(retryIntervalRef.current);
|
||||
retryIntervalRef.current = null;
|
||||
}
|
||||
if (failTimerRef.current) {
|
||||
window.clearTimeout(failTimerRef.current);
|
||||
failTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const clearPlayableUrl = () => {
|
||||
playUrlRef.current = "";
|
||||
originalPlayUrlRef.current = "";
|
||||
upgradedToHttpsRef.current = false;
|
||||
setPlayUrl("");
|
||||
setWarningText("");
|
||||
setOriginalPlayUrl("");
|
||||
};
|
||||
|
||||
const isPlayerReady = () => {
|
||||
const videoEl = playerContainerRef.current?.querySelector("video");
|
||||
return !!videoEl && videoEl.readyState >= 2;
|
||||
};
|
||||
|
||||
const syncPlayableUrl = (url) => {
|
||||
const nextSource = buildPlayableVideoSource(url);
|
||||
playUrlRef.current = nextSource.source;
|
||||
originalPlayUrlRef.current = nextSource.originalSource;
|
||||
upgradedToHttpsRef.current = nextSource.upgradedToHttps;
|
||||
setPlayUrl(nextSource.source);
|
||||
setOriginalPlayUrl(nextSource.originalSource);
|
||||
setWarningText(nextSource.warningText);
|
||||
setPlayerKey(prev => prev + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPlayUrlRef.current = getPlayUrl;
|
||||
}, [getPlayUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearPlayTimers();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const useHikvisionPlayUrl = playSourceType === PLAY_SOURCE_TYPE.HIKVISION;
|
||||
const hasFallbackAddress = videoAddress?.startsWith("http");
|
||||
const canTryPlay = useHikvisionPlayUrl ? (!!cameraNumber || hasFallbackAddress) : hasFallbackAddress;
|
||||
|
||||
requestTaskIdRef.current += 1;
|
||||
const currentTaskId = requestTaskIdRef.current;
|
||||
requestRunningRef.current = false;
|
||||
clearPlayTimers();
|
||||
clearPlayableUrl();
|
||||
setError(null);
|
||||
|
||||
const requestLatestPlayUrl = async () => {
|
||||
if (requestRunningRef.current) {
|
||||
return false;
|
||||
}
|
||||
requestRunningRef.current = true;
|
||||
try {
|
||||
let nextUrl = "";
|
||||
if (useHikvisionPlayUrl && cameraNumber && getPlayUrlRef.current) {
|
||||
const { data } = await getPlayUrlRef.current({ indexCode: cameraNumber });
|
||||
nextUrl = data?.url || data || "";
|
||||
}
|
||||
else if (hasFallbackAddress) {
|
||||
nextUrl = videoAddress;
|
||||
}
|
||||
|
||||
if (requestTaskIdRef.current !== currentTaskId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextUrl) {
|
||||
syncPlayableUrl(nextUrl);
|
||||
setError(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
finally {
|
||||
if (requestTaskIdRef.current === currentTaskId) {
|
||||
requestRunningRef.current = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const attemptPlayback = async () => {
|
||||
if (requestTaskIdRef.current !== currentTaskId || isPlayerReady()) {
|
||||
clearPlayTimers();
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
if (!playUrlRef.current) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
await requestLatestPlayUrl();
|
||||
|
||||
if (requestTaskIdRef.current !== currentTaskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (playUrlRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!canTryPlay) {
|
||||
setLoading(false);
|
||||
setError("暂无视频播放地址");
|
||||
return () => {
|
||||
requestRunningRef.current = false;
|
||||
clearPlayTimers();
|
||||
};
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
attemptPlayback();
|
||||
|
||||
// 首次起流慢时,每 5 秒重新向后端获取一次最新地址,而不是只重建旧播放器实例。
|
||||
retryIntervalRef.current = window.setInterval(() => {
|
||||
attemptPlayback();
|
||||
}, PLAY_RETRY_INTERVAL);
|
||||
|
||||
failTimerRef.current = window.setTimeout(() => {
|
||||
if (requestTaskIdRef.current !== currentTaskId || isPlayerReady()) {
|
||||
clearPlayTimers();
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
clearPlayTimers();
|
||||
setLoading(false);
|
||||
if (!originalPlayUrlRef.current && useHikvisionPlayUrl && cameraNumber) {
|
||||
setError("获取播放地址失败,请检查流媒体服务状态。");
|
||||
return;
|
||||
}
|
||||
setError(getVideoPlaybackFailureText({
|
||||
originalSource: originalPlayUrlRef.current,
|
||||
upgradedToHttps: upgradedToHttpsRef.current,
|
||||
}));
|
||||
}, PLAY_READY_TIMEOUT);
|
||||
|
||||
return () => {
|
||||
requestRunningRef.current = false;
|
||||
clearPlayTimers();
|
||||
};
|
||||
}, [videoAddress, cameraNumber, playSourceType]);
|
||||
|
||||
const bodyStyle = {
|
||||
padding: 0,
|
||||
background: "#000",
|
||||
};
|
||||
|
||||
const playerBoxStyle = {
|
||||
width: "100%",
|
||||
height: 500,
|
||||
background: "#000",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const renderStatusBox = (content) => {
|
||||
return (
|
||||
<div
|
||||
style={playerBoxStyle}
|
||||
>
|
||||
<div style={{ textAlign: "center", maxWidth: 560, padding: "0 24px" }}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
title={videoName || "视频播放"}
|
||||
width={800}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
styles={{ body: bodyStyle }}
|
||||
>
|
||||
{renderStatusBox(
|
||||
<>
|
||||
<VideoCameraOutlined style={{ fontSize: 48, marginBottom: 16, color: "#666" }} />
|
||||
<div style={{ color: "#d9d9d9", lineHeight: "24px" }}>{error}</div>
|
||||
{originalPlayUrl && (
|
||||
<div style={{ marginTop: 16, color: "#8c8c8c", fontSize: 12, wordBreak: "break-all" }}>
|
||||
播放地址:
|
||||
{originalPlayUrl}
|
||||
</div>
|
||||
)}
|
||||
<Button type="primary" style={{ marginTop: 20 }} onClick={onClose}>关闭</Button>
|
||||
</>,
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
title={videoName || "视频播放"}
|
||||
width={800}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
styles={{ body: bodyStyle }}
|
||||
>
|
||||
{renderStatusBox(
|
||||
<>
|
||||
<Spin size="large" style={{ marginBottom: 16 }} />
|
||||
<div>正在加载视频...</div>
|
||||
</>,
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
title={videoName || "视频播放"}
|
||||
width={800}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
styles={{ body: bodyStyle }}
|
||||
>
|
||||
<div ref={playerContainerRef} style={playerBoxStyle}>
|
||||
{playUrl
|
||||
? <Video key={`${playerKey}-${playUrl}`} height="100%" width="100%" source={playUrl} isLive />
|
||||
: <div style={{ textAlign: "center", maxWidth: 560, padding: "0 24px" }}>暂无视频播放地址</div>}
|
||||
</div>
|
||||
{(warningText || originalPlayUrl) && (
|
||||
<div style={{ padding: "12px 16px", background: "#141414", color: "#d9d9d9", fontSize: 12, lineHeight: "20px" }}>
|
||||
{warningText && <div>{warningText}</div>}
|
||||
{originalPlayUrl && (
|
||||
<div style={{ marginTop: warningText ? 8 : 0, wordBreak: "break-all", color: "#8c8c8c" }}>
|
||||
原始地址:
|
||||
{originalPlayUrl}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPlayModal;
|
||||
|
|
@ -8,13 +8,11 @@ import MapSelector from "zy-react-library/components/Map/MapSelector";
|
|||
import Page from "zy-react-library/components/Page";
|
||||
import Search from "zy-react-library/components/Search";
|
||||
import Table from "zy-react-library/components/Table";
|
||||
import Video from "zy-react-library/components/Video";
|
||||
import AliPlayer from "zy-react-library/components/Video/AliPlayer";
|
||||
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender";
|
||||
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
|
||||
import useTable from "zy-react-library/hooks/useTable";
|
||||
import { getLabelName } from "zy-react-library/utils";
|
||||
import { NS_FIRST_LEVEL_DOOR_INFO } from "~/enumerate/namespace";
|
||||
import VideoPlayModal from "./components/VideoPlay";
|
||||
|
||||
const IS_ONLINE_ENUM = [
|
||||
{ name: "否", bianma: "0" },
|
||||
|
|
@ -22,13 +20,14 @@ const IS_ONLINE_ENUM = [
|
|||
];
|
||||
|
||||
function List(props) {
|
||||
const [list, setList] = useState([]);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [addModalVisible, setAddModalVisible] = useState(false);
|
||||
const [currentId, setCurrentId] = useState("");
|
||||
const [infoModalVisible, setInfoModalVisible] = useState(false);
|
||||
const [playModalVisible, setPlayModalVisible] = useState(false);
|
||||
const [playUrl, setPlayUrl] = useState("");
|
||||
const [batchPlayModalVisible, setBatchPlayModalVisible] = useState(false);
|
||||
const [videoModalData, setVideoModalData] = useState({});
|
||||
const [mapModalVisible, setMapModalVisible] = useState(false);
|
||||
const [location, setLocation] = useState({ longitude: "", latitude: "" });
|
||||
|
||||
|
|
@ -37,6 +36,18 @@ function List(props) {
|
|||
const { tableProps, getData } = useTable(props["firstLevelDoorInfoCameraList"], {
|
||||
form,
|
||||
params: { eqForeignId: query.id, eqDeviceType: query.deviceType },
|
||||
onSuccess: async (data) => {
|
||||
const { data: fixedCameraList } = await props["firstLevelDoorInfoFixedCameraList"]({ pageIndex: 1, pageSize: "9999" });
|
||||
for (let i = 0; i < data.list.length; i++) {
|
||||
for (let j = 0; j < fixedCameraList.length; j++) {
|
||||
if (data.list[i].videoResourceId === fixedCameraList[j].id) {
|
||||
data.list[i].isOnline = fixedCameraList[j].isOnline;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setList(data.list);
|
||||
},
|
||||
});
|
||||
|
||||
const onDelete = (id) => {
|
||||
|
|
@ -53,20 +64,25 @@ function List(props) {
|
|||
});
|
||||
};
|
||||
|
||||
const onGetRTSPUrl = async (id) => {
|
||||
// TODO
|
||||
const { success } = await props["firstLevelDoorInfoCameraGetRTSPUrl"]({ id });
|
||||
if (success) {
|
||||
message.success("获取成功");
|
||||
getData();
|
||||
}
|
||||
const onGetPlayUrl = async (videoResourceId) => {
|
||||
const { data } = await props["firstLevelDoorInfoFixedCameraInfo"]({ id: videoResourceId });
|
||||
setPlayModalVisible(true);
|
||||
setVideoModalData(data);
|
||||
};
|
||||
|
||||
const onGetPlayUrl = async (id) => {
|
||||
// TODO
|
||||
const { data } = await props["firstLevelDoorInfoCameraGetPlayUrl"]({ id });
|
||||
setPlayModalVisible(true);
|
||||
setPlayUrl(data);
|
||||
const onSavePosition = async (longitude, latitude) => {
|
||||
const { data } = await props["firstLevelDoorInfoCameraInfo"]({ id: currentId });
|
||||
const { success } = await props["firstLevelDoorInfoCameraEdit"]({
|
||||
...data,
|
||||
longitude,
|
||||
latitude,
|
||||
});
|
||||
if (success) {
|
||||
message.success("保存成功");
|
||||
getData();
|
||||
setCurrentId("");
|
||||
setLocation({ longitude: "", latitude: "" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -75,16 +91,16 @@ function List(props) {
|
|||
form={form}
|
||||
onFinish={getData}
|
||||
options={[
|
||||
{ name: "todo1", label: "视频名称" },
|
||||
{ name: "todo3", label: "是否在线", render: FORM_ITEM_RENDER_ENUM.SELECT, items: IS_ONLINE_ENUM },
|
||||
{ name: "videoResourceName", label: "视频名称" },
|
||||
]}
|
||||
/>
|
||||
<Table
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
preserveSelectedRowKeys: true,
|
||||
onChange: (selectedRowKeys) => {
|
||||
onChange: (selectedRowKeys, selectedRows) => {
|
||||
setSelectedRowKeys(selectedRowKeys);
|
||||
setSelectedRows(selectedRows);
|
||||
},
|
||||
}}
|
||||
toolBarRender={() => (
|
||||
|
|
@ -109,7 +125,15 @@ function List(props) {
|
|||
message.error("请选择要播放的视频");
|
||||
return;
|
||||
}
|
||||
setBatchPlayModalVisible(true);
|
||||
const videoResourceData = selectedRows.map((row) => {
|
||||
return {
|
||||
videoName: row.videoResourceName,
|
||||
id: row.videoResourceId,
|
||||
cameraNumber: row.videoResourceCode,
|
||||
isOnline: row.isOnline,
|
||||
};
|
||||
});
|
||||
props.history.push(`./batchPlay?videoResourceData=${JSON.stringify(videoResourceData)}`);
|
||||
}}
|
||||
>
|
||||
播放全部
|
||||
|
|
@ -118,22 +142,21 @@ function List(props) {
|
|||
)}
|
||||
columns={[
|
||||
{ dataIndex: "videoResourceName", title: "视频名称" },
|
||||
{ dataIndex: "todo2", title: "播放地址" },
|
||||
{
|
||||
dataIndex: "todo3",
|
||||
dataIndex: "longitude",
|
||||
title: "视频定位状态",
|
||||
width: 120,
|
||||
render: (text, record) => (record.longitude && record.latitude ? "已定位" : "未定位"),
|
||||
},
|
||||
{
|
||||
dataIndex: "todo4",
|
||||
dataIndex: "isOnline",
|
||||
title: "是否在线",
|
||||
width: 100,
|
||||
render: (_, record) => getLabelName({ list: IS_ONLINE_ENUM, status: record.todo4 }),
|
||||
render: (_, record) => getLabelName({ list: IS_ONLINE_ENUM, status: record.isOnline }),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 400,
|
||||
width: 230,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
|
|
@ -162,6 +185,7 @@ function List(props) {
|
|||
onClick={() => {
|
||||
setMapModalVisible(true);
|
||||
setLocation({ longitude: record.longitude, latitude: record.latitude });
|
||||
setCurrentId(record.id);
|
||||
}}
|
||||
>
|
||||
定位
|
||||
|
|
@ -181,26 +205,22 @@ function List(props) {
|
|||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
onGetPlayUrl(record.id);
|
||||
if (record.isOnline === 1) {
|
||||
onGetPlayUrl(record.videoResourceId);
|
||||
}
|
||||
else {
|
||||
message.error("视频资源离线");
|
||||
}
|
||||
}}
|
||||
>
|
||||
播放
|
||||
</Button>
|
||||
{props.entrance !== "enterprise" && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
onGetRTSPUrl(record.id);
|
||||
}}
|
||||
>
|
||||
获取rtsp地址
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
{...tableProps}
|
||||
dataSource={list}
|
||||
/>
|
||||
{
|
||||
addModalVisible && (
|
||||
|
|
@ -228,23 +248,16 @@ function List(props) {
|
|||
}
|
||||
{
|
||||
playModalVisible && (
|
||||
<Video
|
||||
onCancel={() => {
|
||||
<VideoPlayModal
|
||||
videoName={videoModalData.videoName}
|
||||
videoAddress={videoModalData.videoAddress}
|
||||
cameraNumber={videoModalData.cameraNumber}
|
||||
getPlayUrl={props["firstLevelDoorInfoFixedCameraGetPlayUrl"]}
|
||||
playSourceType="hikvision"
|
||||
onClose={() => {
|
||||
setPlayModalVisible(false);
|
||||
setPlayUrl("");
|
||||
setVideoModalData({});
|
||||
}}
|
||||
visible={playModalVisible}
|
||||
source={playUrl}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
batchPlayModalVisible && (
|
||||
<BatchPlayModal
|
||||
onCancel={() => {
|
||||
setBatchPlayModalVisible(false);
|
||||
}}
|
||||
ids={selectedRowKeys}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -255,6 +268,9 @@ function List(props) {
|
|||
longitude={location.longitude}
|
||||
latitude={location.latitude}
|
||||
type="cesium"
|
||||
onConfirm={(longitude, latitude) => {
|
||||
onSavePosition(longitude, latitude);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
|
|
@ -281,6 +297,7 @@ const AddModalComponent = (props) => {
|
|||
id: props.id,
|
||||
foreignId: props.query.id,
|
||||
deviceType: props.query.deviceType,
|
||||
videoType: 2,
|
||||
});
|
||||
if (success) {
|
||||
props.onCancel();
|
||||
|
|
@ -332,9 +349,9 @@ const AddModalComponent = (props) => {
|
|||
setSelectFixedCameraModalVisible(false);
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
form.setFieldValue("videoResourceName", values.todo);
|
||||
form.setFieldValue("videoResourceId", values.todo);
|
||||
form.setFieldValue("videoResourceCode", values.todo);
|
||||
form.setFieldValue("videoResourceName", values.videoName);
|
||||
form.setFieldValue("videoResourceId", values.id);
|
||||
form.setFieldValue("videoResourceCode", values.cameraNumber);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
@ -345,7 +362,6 @@ const AddModalComponent = (props) => {
|
|||
|
||||
const SelectFixedCameraModalComponent = (props) => {
|
||||
const [form] = FormBuilder.useForm();
|
||||
// TODO
|
||||
const { tableProps, getData } = useTable(props["firstLevelDoorInfoFixedCameraList"], {
|
||||
form,
|
||||
useStorageQueryCriteria: false,
|
||||
|
|
@ -366,15 +382,15 @@ const SelectFixedCameraModalComponent = (props) => {
|
|||
form={form}
|
||||
onFinish={getData}
|
||||
options={[
|
||||
{ name: "todo1", label: "视频名称" },
|
||||
{ name: "videoName", label: "视频名称" },
|
||||
]}
|
||||
/>
|
||||
<Table
|
||||
options={false}
|
||||
disabledResizer={true}
|
||||
columns={[
|
||||
{ dataIndex: "todo1", title: "视频名称" },
|
||||
{ dataIndex: "todo2", title: "区域" },
|
||||
{ dataIndex: "videoName", title: "视频名称" },
|
||||
{ dataIndex: "corpinfoName", title: "所属单位" },
|
||||
{
|
||||
title: "操作",
|
||||
width: 120,
|
||||
|
|
@ -435,42 +451,8 @@ const InfoModalComponent = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const BatchPlayModalComponent = (props) => {
|
||||
const [playUrl, setPlayUrl] = useState([]);
|
||||
|
||||
const getData = async () => {
|
||||
// TODO
|
||||
const { data } = await props["firstLevelDoorInfoCameraGetBatchPlayUrl"]({ ids: props.ids });
|
||||
setPlayUrl(data);
|
||||
};
|
||||
useEffect(() => {
|
||||
props.id && getData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onCancel={props.onCancel}
|
||||
title="视频"
|
||||
maskClosable={false}
|
||||
loading={props.firstLevelDoorInfo.firstLevelDoorInfoCameraLoading}
|
||||
width={1200}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={props.onCancel}>取消</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 20 }}>
|
||||
{playUrl.map((item, index) => (
|
||||
<AliPlayer key={index} height="300px" source={item} />
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const AddModal = Connect([NS_FIRST_LEVEL_DOOR_INFO], true)(AddModalComponent);
|
||||
const SelectFixedCameraModal = Connect([NS_FIRST_LEVEL_DOOR_INFO], true)(SelectFixedCameraModalComponent);
|
||||
const InfoModal = Connect([NS_FIRST_LEVEL_DOOR_INFO], true)(InfoModalComponent);
|
||||
const BatchPlayModal = Connect([NS_FIRST_LEVEL_DOOR_INFO], true)(BatchPlayModalComponent);
|
||||
|
||||
export default Connect([NS_FIRST_LEVEL_DOOR_INFO], true)(List);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import BatchPlayPage from "../../../Camera/BatchPlay";
|
||||
|
||||
function BatchPlay(props) {
|
||||
return (<BatchPlayPage {...props} />);
|
||||
}
|
||||
|
||||
export default BatchPlay;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import CameraPage from "../../../Camera/List";
|
||||
|
||||
function Camera(props) {
|
||||
return (<CameraPage {...props} />);
|
||||
}
|
||||
|
||||
export default Camera;
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import CameraPage from "../../Camera/List";
|
||||
|
||||
function Camera() {
|
||||
return (<CameraPage />);
|
||||
function Camera(props) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
export default Camera;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import Table from "zy-react-library/components/Table";
|
|||
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender";
|
||||
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
|
||||
import useTable from "zy-react-library/hooks/useTable";
|
||||
import { CURRENT_IN_PORT_STATUS_ENUM } from "~/enumerate/constant";
|
||||
import { NS_VEHICLE_APPLY } from "~/enumerate/namespace";
|
||||
|
||||
function List(props) {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ function Add(props) {
|
|||
}, []);
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
if (values.drivingLicenseFile.length !== 2) {
|
||||
message.error("请上传两张驾驶证");
|
||||
return;
|
||||
}
|
||||
await deleteFile({ single: false, files: values.drivingLicenseDeleteFile });
|
||||
await deleteFile({ single: false, files: values.attachmentDeleteFile });
|
||||
const { id: drivingLicenseId } = await uploadFile({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { Button, Modal,message, Space } from "antd";
|
||||
import { Button, message, Modal, Space } from "antd";
|
||||
import Page from "zy-react-library/components/Page";
|
||||
import Search from "zy-react-library/components/Search";
|
||||
import Table from "zy-react-library/components/Table";
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function List(props) {
|
|||
{ title: "是否录入身份证号", dataIndex: "userCard", render: (_, record) => record.userCard ? "是" : "否" },
|
||||
{
|
||||
title: "操作",
|
||||
width: 200,
|
||||
width: 300,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ function List(props) {
|
|||
{
|
||||
title: "操作",
|
||||
fixed: "right",
|
||||
width: 200,
|
||||
width: 350,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import AddIcon from "zy-react-library/components/Icon/AddIcon";
|
|||
import Page from "zy-react-library/components/Page";
|
||||
import Search from "zy-react-library/components/Search";
|
||||
import DictionarySelect from "zy-react-library/components/Select/Dictionary";
|
||||
import DepartmentSelectTree from "zy-react-library/components/SelectTree/Department/Gwj";
|
||||
import Table from "zy-react-library/components/Table";
|
||||
import Upload from "zy-react-library/components/Upload";
|
||||
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender";
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import useGetFile from "zy-react-library/hooks/useGetFile";
|
|||
import useTable from "zy-react-library/hooks/useTable";
|
||||
import { getLabelName } from "zy-react-library/utils";
|
||||
import { NS_TEMPORARY_PERSONNEL } from "~/enumerate/namespace";
|
||||
import {xgfPersonnelList} from "~/api/temporaryPersonnel";
|
||||
|
||||
const STATUS_ENUM = [
|
||||
{ bianma: "1", name: "审核中" },
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function Add(props) {
|
|||
{ userFaceUrl },
|
||||
{ employeePersonUserName: values.employeePersonUserName },
|
||||
{ userPhone: values.userPhone },
|
||||
{ userCard: values.userCard },
|
||||
{ userCard: btoa(values.userCard) },
|
||||
],
|
||||
gateLevelAuthArea: JSON.stringify({ area: values.area }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -96,7 +96,11 @@ function List(props) {
|
|||
columns={[
|
||||
{ title: "访问人姓名", dataIndex: "employeePersonUserName" },
|
||||
{ title: "手机号", dataIndex: "userPhone" },
|
||||
{ title: "身份证号", dataIndex: "userCard" },
|
||||
{
|
||||
title: "身份证号",
|
||||
dataIndex: "userCard",
|
||||
render: (_, record) => record.userCard ? atob(record.userCard) : "",
|
||||
},
|
||||
{ title: "来访事由", dataIndex: "reasonVisit" },
|
||||
{ title: "访问开始时间", dataIndex: "visitStartTime" },
|
||||
{ title: "访问结束时间", dataIndex: "visitEndTime" },
|
||||
|
|
@ -199,7 +203,10 @@ const QrCodeModal = (props) => {
|
|||
]}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<QRCode value={`${window.location.origin}/primeport-h5/container/mobile/firstLevelDoor/personnelApplication/apply`} style={{ margin: "0 auto", marginBottom: 10 }} />
|
||||
<QRCode
|
||||
value={`${window.location.origin}/primeport-h5/container/mobile/firstLevelDoor/personnelApplication/apply`}
|
||||
style={{ margin: "0 auto", marginBottom: 10 }}
|
||||
/>
|
||||
<div>温馨提示:此二维码支持临时入港申请预约及预约结果查询</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import PreviewImg from "zy-react-library/components/PreviewImg";
|
|||
import { UPLOAD_FILE_TYPE_ENUM } from "zy-react-library/enum/uploadFile/gwj";
|
||||
import useGetFile from "zy-react-library/hooks/useGetFile";
|
||||
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
|
||||
import { getLabelName } from "zy-react-library/utils";
|
||||
import { NS_TEMPORARY_PERSONNEL } from "~/enumerate/namespace";
|
||||
import {getLabelName} from "zy-react-library/utils";
|
||||
|
||||
const VEHICLE_APPROVAL_STATUS_ENUM = [
|
||||
{ bianma: 1, name: "审批中" },
|
||||
{ bianma: 2, name: "通过" },
|
||||
|
|
@ -17,13 +18,10 @@ function View(props) {
|
|||
const query = useGetUrlQuery();
|
||||
const { loading: getFileLoading, getFile } = useGetFile();
|
||||
|
||||
const [ info, setInfo ] = useState({});
|
||||
const [info, setInfo] = useState({});
|
||||
|
||||
const getData = async () => {
|
||||
console.log(".temporaryPersonnel.temporaryPersonnelLoading")
|
||||
console.log(NS_TEMPORARY_PERSONNEL)
|
||||
const { data } = await props["temporaryPersonnelInfo"]({ id: query.id });
|
||||
console.log(data)
|
||||
const informSignFile = await getFile({ eqType: UPLOAD_FILE_TYPE_ENUM[611], eqForeignKey: data.informSignId });
|
||||
setInfo({
|
||||
...data,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ function Add(props) {
|
|||
}, [gateLevelAuthArea]);
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
if (values.drivingLicenseFile.length !== 2) {
|
||||
message.error("请上传两张驾驶证");
|
||||
return;
|
||||
}
|
||||
const { id: drivingLicenseId } = await uploadFile({
|
||||
single: false,
|
||||
files: values.drivingLicenseFile,
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ import PreviewImg from "zy-react-library/components/PreviewImg";
|
|||
import { UPLOAD_FILE_TYPE_ENUM } from "zy-react-library/enum/uploadFile/gwj";
|
||||
import useGetFile from "zy-react-library/hooks/useGetFile";
|
||||
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
|
||||
import { getLabelName } from "zy-react-library/utils";
|
||||
import { VEHICLE_AUDIT_STATUS_ENUM } from "~/enumerate/constant";
|
||||
import { NS_VEHICLE_APPLY } from "~/enumerate/namespace";
|
||||
import {getLabelName} from "zy-react-library/utils";
|
||||
import {VEHICLE_AUDIT_STATUS_ENUM} from "~/enumerate/constant";
|
||||
|
||||
function View(props) {
|
||||
const query = useGetUrlQuery();
|
||||
const { loading: getFileLoading, getFile } = useGetFile();
|
||||
|
||||
const [ info, setInfo ] = useState({});
|
||||
const [info, setInfo] = useState({});
|
||||
const getData = async () => {
|
||||
const { data } = await props["vehicleApplyInfo"]({ id: query.id });
|
||||
const drivingLicenseFile = await getFile({
|
||||
|
|
@ -72,7 +72,7 @@ function View(props) {
|
|||
{ label: "审批企业", children: item.auditCorpName },
|
||||
{ label: "审批部门", children: item.auditDeptName },
|
||||
{ label: "审批人", children: item.auditUserName },
|
||||
{ label: "审批状态", children: getLabelName({ list: VEHICLE_AUDIT_STATUS_ENUM, status: item.auditStatus+'' }) },
|
||||
{ label: "审批状态", children: getLabelName({ list: VEHICLE_AUDIT_STATUS_ENUM, status: `${item.auditStatus}` }) },
|
||||
...(item.auditStatus === 0 ? [] : [{ label: "审批时间", children: item.auditTime }, { label: "驳回原因", children: item.remarks }]),
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
const getTrimmedUrl = (url) => {
|
||||
if (typeof url !== "string") {
|
||||
return "";
|
||||
}
|
||||
return url.trim();
|
||||
};
|
||||
|
||||
export const buildPlayableVideoSource = (url) => {
|
||||
const originalSource = getTrimmedUrl(url);
|
||||
if (!originalSource) {
|
||||
return {
|
||||
source: "",
|
||||
originalSource: "",
|
||||
warningText: "",
|
||||
upgradedToHttps: false,
|
||||
};
|
||||
}
|
||||
|
||||
const isSecurePage = typeof window !== "undefined" && window.location.protocol === "https:";
|
||||
|
||||
return {
|
||||
source: originalSource,
|
||||
originalSource,
|
||||
warningText: isSecurePage && originalSource.startsWith("http://")
|
||||
? "当前页面是 HTTPS,播放地址是 HTTP,浏览器可能拦截该视频流,请检查流媒体服务协议、代理或浏览器安全策略。"
|
||||
: "",
|
||||
upgradedToHttps: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getVideoPlaybackFailureText = ({ originalSource = "", upgradedToHttps = false } = {}) => {
|
||||
if (!originalSource) {
|
||||
return "暂无视频播放地址";
|
||||
}
|
||||
|
||||
if (upgradedToHttps) {
|
||||
return "播放器已尝试调整播放地址协议,但仍未拿到可播放画面,请检查流媒体服务协议、证书或代理配置。";
|
||||
}
|
||||
|
||||
return "播放器已拿到播放地址,但仍未返回可播放画面,请检查流地址、跨域配置或流媒体服务状态。";
|
||||
};
|
||||
Loading…
Reference in New Issue