bug:16193、16192、16191、16190、16189、16179、16173、16168、16164、16160、16152、16150

master
LiuJiaNan 2026-04-28 17:06:36 +08:00
parent 2ae282f3fe
commit 0b5bde42a8
47 changed files with 1361 additions and 187 deletions

View File

@ -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`,

View File

@ -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`,

View File

@ -4,10 +4,11 @@ export const enclosedEnterprisePersonnelPermissionsList = declareRequest(
"enclosedEnterprisePersonnelPermissionsLoading",
`Post > @/primeport/closedAreaPersonApply/getCorpUserList`,
);
export const enclosedEnterprisePersonnelPermissionsPersonnelRecordsList = declareRequest(
export const enclosedEnterprisePersonnelPermissionsPersonnelRecordsList
= declareRequest(
"enclosedEnterprisePersonnelPermissionsLoading",
`Post > @/primeport/`,
);
);
export const enclosedEnterprisePersonnelPermissionsInfo = declareRequest(
"enclosedEnterprisePersonnelPermissionsLoading",
`Get > /primeport/closedAreaPersonApply/getAuthorizationPersonInfo/{id}`,

View File

@ -4,11 +4,13 @@ export const enclosedPersonnelAndVehicleStatisticsList = declareRequest(
"enclosedPersonnelAndVehicleStatisticsLoading",
`Post > @/primeport/`,
);
export const enclosedPersonnelAndVehicleStatisticsVehicleEntryAndExitRecordsList = declareRequest(
export const enclosedPersonnelAndVehicleStatisticsVehicleEntryAndExitRecordsList
= declareRequest(
"enclosedPersonnelAndVehicleStatisticsLoading",
`Post > @/primeport/`,
);
export const enclosedPersonnelAndVehicleStatisticsPersonnelEntryAndExitRecordsList = declareRequest(
);
export const enclosedPersonnelAndVehicleStatisticsPersonnelEntryAndExitRecordsList
= declareRequest(
"enclosedPersonnelAndVehicleStatisticsLoading",
`Post > @/primeport/`,
);
);

View File

@ -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",

View File

@ -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/`);

View File

@ -0,0 +1,7 @@
import BatchPlayPage from "~/pages/Container/Supervision/EnclosedArea/AreaAndEntrance/EnclosedAreaDoor/Camera/BatchPlay";
function BatchPlay(props) {
return (<BatchPlayPage {...props} />);
}
export default BatchPlay;

View File

@ -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;

View File

@ -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;

View File

@ -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" },
{

View File

@ -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 },

View File

@ -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" },
{

View File

@ -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" },

View File

@ -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 },

View File

@ -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" },

View File

@ -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" />

View File

@ -1,4 +1,5 @@
.adm-list, .adm-input {
.adm-list,
.adm-input {
--font-size: var(--adm-font-size-6);
}

View File

@ -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,

View File

@ -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) {

View File

@ -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 },

View File

@ -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: " ",

View File

@ -113,6 +113,7 @@ function Add(props) {
render: (
<DepartmentSelectTree
searchType="inType"
level={1}
params={{ enterpriseType: [2] }}
onChange={(value) => {
if (value) {

View File

@ -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 },

View File

@ -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={() => {

View File

@ -0,0 +1,7 @@
import BatchPlayPage from "~/pages/Container/Supervision/FirstLevelDoor/BasicInfo/FirstLevelDoorInfo/Camera/BatchPlay";
function BatchPlay(props) {
return (<BatchPlayPage {...props} />);
}
export default BatchPlay;

View File

@ -0,0 +1,7 @@
import BatchPlayPage from "../../../Camera/BatchPlay";
function BatchPlay(props) {
return (<BatchPlayPage {...props} />);
}
export default BatchPlay;

View File

@ -0,0 +1,7 @@
import CameraPage from "../../../Camera/List";
function Camera(props) {
return (<CameraPage {...props} />);
}
export default Camera;

View File

@ -1,7 +1,5 @@
import CameraList from "../../Camera/List";
function Camera() {
return (<CameraList />);
function Camera(props) {
return props.children;
}
export default Camera;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -0,0 +1,7 @@
import BatchPlayPage from "../../../Camera/BatchPlay";
function BatchPlay(props) {
return (<BatchPlayPage {...props} />);
}
export default BatchPlay;

View File

@ -0,0 +1,7 @@
import CameraPage from "../../../Camera/List";
function Camera(props) {
return (<CameraPage {...props} />);
}
export default Camera;

View File

@ -1,7 +1,5 @@
import CameraPage from "../../Camera/List";
function Camera() {
return (<CameraPage />);
function Camera(props) {
return props.children;
}
export default Camera;

View File

@ -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) {

View File

@ -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({

View File

@ -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";

View File

@ -98,7 +98,7 @@ function List(props) {
{ title: "是否录入身份证号", dataIndex: "userCard", render: (_, record) => record.userCard ? "是" : "否" },
{
title: "操作",
width: 200,
width: 300,
fixed: "right",
render: (_, record) => (
<Space>

View File

@ -94,7 +94,7 @@ function List(props) {
{
title: "操作",
fixed: "right",
width: 200,
width: 350,
render: (_, record) => (
<Space>
<Button

View File

@ -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";

View File

@ -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: "审核中" },

View File

@ -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 }),
});

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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 }]),
]}
/>

41
src/utils/video.js Normal file
View File

@ -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 "播放器已拿到播放地址,但仍未返回可播放画面,请检查流地址、跨域配置或流媒体服务状态。";
};