feat(map): 添加人员轨迹功能及筛选组件
parent
273aa353ed
commit
f32d44bad9
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
|
||||||
|
|
||||||
|
// 人员轨迹按终端或人员编号、时间范围查询,接口参数由轨迹筛选面板传入。
|
||||||
|
export const getPeopleTrajectory = declareRequest(
|
||||||
|
"getPeopleTrajectoryLoading",
|
||||||
|
"Post > @/personnelPosition/bi/personTrace",
|
||||||
|
);
|
||||||
|
|
@ -9,3 +9,4 @@ export const NS_GLOBAL = defineNamespace("global");
|
||||||
export const NS_BI_STATISTICS = defineNamespace("biStatistics");
|
export const NS_BI_STATISTICS = defineNamespace("biStatistics");
|
||||||
export const NS_BI_MARK = defineNamespace("biMark");
|
export const NS_BI_MARK = defineNamespace("biMark");
|
||||||
export const NS_BI_MARK_INFO = defineNamespace("biMarkInfo");
|
export const NS_BI_MARK_INFO = defineNamespace("biMarkInfo");
|
||||||
|
export const NS_BI_PEOPLE = defineNamespace("biPeople");
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { NS_BI_MARK } from "~/enumerate/namespace";
|
||||||
import { branchOfficeUtilsList } from "~/pages/Container/Map/components/BottomUtils/branchOfficeUtilsList";
|
import { branchOfficeUtilsList } from "~/pages/Container/Map/components/BottomUtils/branchOfficeUtilsList";
|
||||||
import { Context } from "~/pages/Container/Map/js/context";
|
import { Context } from "~/pages/Container/Map/js/context";
|
||||||
import usePeoplePosition from "~/pages/Container/Map/js/usePeoplePosition";
|
import usePeoplePosition from "~/pages/Container/Map/js/usePeoplePosition";
|
||||||
|
import PeopleTrajectorySelect from "~/pages/Container/Map/components/PeopleTrajectorySelect";
|
||||||
import { portUtilsList } from "./portUtilsList";
|
import { portUtilsList } from "./portUtilsList";
|
||||||
import { usePortUtilsAnimation } from "./usePortUtilsAnimation";
|
import { usePortUtilsAnimation } from "./usePortUtilsAnimation";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
@ -67,7 +68,12 @@ function BottomUtils(props) {
|
||||||
portArea,
|
portArea,
|
||||||
} = useContext(Context);
|
} = useContext(Context);
|
||||||
|
|
||||||
const { connect: peoplePositionConnect, disconnect: peoplePositionDisconnect } = usePeoplePosition(mapMethods, currentBranchOffice);
|
const [peopleList, setPeopleList] = useState([]);
|
||||||
|
const { connect: peoplePositionConnect, disconnect: peoplePositionDisconnect } = usePeoplePosition(
|
||||||
|
mapMethods,
|
||||||
|
currentBranchOffice,
|
||||||
|
setPeopleList,
|
||||||
|
);
|
||||||
|
|
||||||
const bottomUtilsMode = !pureMap && currentPort && !currentBranchOffice
|
const bottomUtilsMode = !pureMap && currentPort && !currentBranchOffice
|
||||||
? "port"
|
? "port"
|
||||||
|
|
@ -92,6 +98,10 @@ function BottomUtils(props) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetAllCheck = () => {
|
const resetAllCheck = () => {
|
||||||
|
// 底部工具栏被外部操作重置时,人员定位的 WebSocket 和 Cesium 实体不会随勾选状态自动销毁,需要统一清理。
|
||||||
|
peoplePositionDisconnect();
|
||||||
|
mapMethods.current.removePeoplePointAll();
|
||||||
|
setPeopleList([]);
|
||||||
setList(produce((draft) => {
|
setList(produce((draft) => {
|
||||||
draft.forEach((item) => {
|
draft.forEach((item) => {
|
||||||
item.list.forEach((item1) => {
|
item.list.forEach((item1) => {
|
||||||
|
|
@ -181,6 +191,7 @@ function BottomUtils(props) {
|
||||||
if (!check) {
|
if (!check) {
|
||||||
if (item.type === "peoplePosition") {
|
if (item.type === "peoplePosition") {
|
||||||
resetPeopleTrajectory(index);
|
resetPeopleTrajectory(index);
|
||||||
|
mapMethods.current.removePeopleTrajectory();
|
||||||
peoplePositionDisconnect();
|
peoplePositionDisconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +220,8 @@ function BottomUtils(props) {
|
||||||
await addMarkPoint(item);
|
await addMarkPoint(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPeopleTrajectoryChecked = list.some(item => item.list?.some(option => option.type === "peopleTrajectory" && option.check));
|
||||||
|
|
||||||
const renderPortUtils = () => {
|
const renderPortUtils = () => {
|
||||||
if (bottomUtilsMode === "port" && listMode === bottomUtilsMode) {
|
if (bottomUtilsMode === "port" && listMode === bottomUtilsMode) {
|
||||||
return list.map((item, index) => {
|
return list.map((item, index) => {
|
||||||
|
|
@ -313,6 +326,18 @@ function BottomUtils(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map_content_bottom_utils_container">
|
<div className="map_content_bottom_utils_container">
|
||||||
|
<AnimatePresence>
|
||||||
|
{isPeopleTrajectoryChecked && (
|
||||||
|
<motion.div
|
||||||
|
className="people_trajectory_select"
|
||||||
|
initial={{ y: 20, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 20, opacity: 0 }}
|
||||||
|
>
|
||||||
|
<PeopleTrajectorySelect peopleList={peopleList} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{bottomUtilsMode && (
|
{bottomUtilsMode && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||||
|
import { Button, DatePicker, Form, message, Select, Spin } from "antd";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
import { NS_BI_PEOPLE } from "~/enumerate/namespace";
|
||||||
|
import { Context } from "~/pages/Container/Map/js/context";
|
||||||
|
import "./index.less";
|
||||||
|
|
||||||
|
const MAX_TRAJECTORY_RANGE = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function PeopleTrajectorySelect(props) {
|
||||||
|
const { mapMethods } = useContext(Context);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [queryLoading, setQueryLoading] = useState(false);
|
||||||
|
|
||||||
|
const peopleOptions = useMemo(
|
||||||
|
() => props.peopleList.map(person => ({
|
||||||
|
value: person.terminalNo || person.staffNo,
|
||||||
|
label: person.staffName,
|
||||||
|
})),
|
||||||
|
[props.peopleList],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => mapMethods.current.removePeopleTrajectory();
|
||||||
|
}, [mapMethods]);
|
||||||
|
|
||||||
|
const handleQuery = async () => {
|
||||||
|
const { personId, timeRange } = await form.validateFields();
|
||||||
|
const [startTime, endTime] = timeRange.map(time => dayjs(time).valueOf());
|
||||||
|
if (endTime - startTime > MAX_TRAJECTORY_RANGE) {
|
||||||
|
message.warning("时间范围不能超过24小时");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const person = props.peopleList.find(item => (item.terminalNo || item.staffNo) === personId);
|
||||||
|
const params = { startTime, endTime };
|
||||||
|
if (person?.terminalNo)
|
||||||
|
params.terminalNo = person.terminalNo;
|
||||||
|
else
|
||||||
|
params.staffNo = person?.staffNo;
|
||||||
|
|
||||||
|
setQueryLoading(true);
|
||||||
|
try {
|
||||||
|
const { data } = await props.getPeopleTrajectory(params);
|
||||||
|
const points = data?.points || [];
|
||||||
|
if (points.length < 2) {
|
||||||
|
mapMethods.current.removePeopleTrajectory();
|
||||||
|
message.warning("该时间范围内没有可绘制的轨迹");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapMethods.current.addPeopleTrajectory(points);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setQueryLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
form.resetFields();
|
||||||
|
mapMethods.current.removePeopleTrajectory();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="people_trajectory_select__content">
|
||||||
|
<Spin spinning={queryLoading}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
styles={{ label: { color: "#fff" } }}
|
||||||
|
className="people_trajectory_select__form"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
className="people_trajectory_select__person"
|
||||||
|
label="人员"
|
||||||
|
name="personId"
|
||||||
|
rules={[{ required: true, message: "请选择人员" }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
options={peopleOptions}
|
||||||
|
placeholder="请选择人员"
|
||||||
|
styles={{
|
||||||
|
root: { color: "#fff", backgroundColor: "rgba(2, 30, 81, 0.85)", borderColor: "rgba(2, 30, 81, 1)" },
|
||||||
|
placeholder: { color: "#fff" },
|
||||||
|
suffix: { color: "#fff" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
className="people_trajectory_select__time"
|
||||||
|
label="时间范围"
|
||||||
|
name="timeRange"
|
||||||
|
rules={[{ required: true, message: "请选择时间范围" }]}
|
||||||
|
>
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
showTime
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
color: "#fff",
|
||||||
|
backgroundColor: "rgba(2, 30, 81, 0.85)",
|
||||||
|
borderColor: "rgba(2, 30, 81, 1)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<div className="people_trajectory_select__actions">
|
||||||
|
<Button type="primary" loading={queryLoading} onClick={handleQuery}>查询</Button>
|
||||||
|
<Button onClick={handleReset}>重置</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Connect([NS_BI_PEOPLE], true)(PeopleTrajectorySelect);
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
.people_trajectory_select {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 180px;
|
||||||
|
left: calc(50% - 500px);
|
||||||
|
z-index: 2;
|
||||||
|
width: 900px;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(6, 24, 64, 0.94);
|
||||||
|
border: 1px solid rgba(70, 174, 255, 0.7);
|
||||||
|
box-shadow: 0 0 16px rgba(36, 136, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{ant-prefix}-form-item {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.@{ant-prefix}-select-selector,
|
||||||
|
.@{ant-prefix}-picker {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(2, 30, 81, 0.85);
|
||||||
|
border-color: rgba(2, 30, 81, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{ant-prefix}-picker-input > input::placeholder,
|
||||||
|
.@{ant-prefix}-picker-suffix,
|
||||||
|
.@{ant-prefix}-picker-separator {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{ant-prefix}-picker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__person {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,7 @@ const peopleImg = {
|
||||||
red: people_red,
|
red: people_red,
|
||||||
yellow: people_yellow,
|
yellow: people_yellow,
|
||||||
}; // 人员定位颜色图片
|
}; // 人员定位颜色图片
|
||||||
|
const peopleTrajectoryEntityId = "peopleTrajectoryEntity";
|
||||||
|
|
||||||
const Cesium = window.Cesium;
|
const Cesium = window.Cesium;
|
||||||
|
|
||||||
|
|
@ -410,6 +411,29 @@ export default function useMapMethods(viewerRef, request) {
|
||||||
removeEntityCollection(getViewer(), `markEntityCollectionMerged${markType ? `_${markType}` : ""}`);
|
removeEntityCollection(getViewer(), `markEntityCollectionMerged${markType ? `_${markType}` : ""}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 查询新轨迹前先移除旧轨迹,确保地图上始终只展示当前筛选结果。
|
||||||
|
const removePeopleTrajectory = () => {
|
||||||
|
getViewer().entities.removeById(peopleTrajectoryEntityId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将接口返回的经纬高点位绘制为动态轨迹线,少于两个点时不创建无意义的线段。
|
||||||
|
const addPeopleTrajectory = (points = []) => {
|
||||||
|
removePeopleTrajectory();
|
||||||
|
if (points.length < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const coordinates = points.flatMap(point => [point.lon, point.lat, point.alt || 0]);
|
||||||
|
getViewer().entities.add({
|
||||||
|
id: peopleTrajectoryEntityId,
|
||||||
|
polyline: {
|
||||||
|
positions: Cesium.Cartesian3.fromDegreesArrayHeights(coordinates),
|
||||||
|
width: 5,
|
||||||
|
material: new Cesium.TrajectoryPolylineTrailLinkMaterialProperty(getViewer()),
|
||||||
|
clampToGround: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 人员定位使用 Viewer 的全局时钟;每次开启定位仅初始化一次,避免首批点位互相重置时钟。
|
// 人员定位使用 Viewer 的全局时钟;每次开启定位仅初始化一次,避免首批点位互相重置时钟。
|
||||||
const initPeoplePositionClock = () => {
|
const initPeoplePositionClock = () => {
|
||||||
const start = Cesium.JulianDate.now();
|
const start = Cesium.JulianDate.now();
|
||||||
|
|
@ -492,6 +516,7 @@ export default function useMapMethods(viewerRef, request) {
|
||||||
});
|
});
|
||||||
peopleId.current = [];
|
peopleId.current = [];
|
||||||
peoplePositionStartTime.current = null;
|
peoplePositionStartTime.current = null;
|
||||||
|
removePeopleTrajectory();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -509,6 +534,8 @@ export default function useMapMethods(viewerRef, request) {
|
||||||
removeWall,
|
removeWall,
|
||||||
addMarkPoint,
|
addMarkPoint,
|
||||||
removeMarkPoint,
|
removeMarkPoint,
|
||||||
|
addPeopleTrajectory,
|
||||||
|
removePeopleTrajectory,
|
||||||
initPeoplePositionClock,
|
initPeoplePositionClock,
|
||||||
addPeoplePoint,
|
addPeoplePoint,
|
||||||
updatePeoplePoint,
|
updatePeoplePoint,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useRef } from "react";
|
||||||
import edgeQHD from "./edge/qhd.js";
|
import edgeQHD from "./edge/qhd.js";
|
||||||
import { filterNull, isPointWithinTheArea } from "./utils";
|
import { filterNull, isPointWithinTheArea } from "./utils";
|
||||||
|
|
||||||
export default function usePeoplePosition(mapMethods, currentBranchOffice) {
|
export default function usePeoplePosition(mapMethods, currentBranchOffice, onPeopleChange) {
|
||||||
const markType = "peoplePosition";
|
const markType = "peoplePosition";
|
||||||
const points = useRef([]);
|
const points = useRef([]);
|
||||||
|
|
||||||
|
|
@ -43,6 +43,8 @@ export default function usePeoplePosition(mapMethods, currentBranchOffice) {
|
||||||
mapMethods.current && (await mapMethods.current.addPeoplePoint(perLoc, { markType, isShowModal: false }));
|
mapMethods.current && (await mapMethods.current.addPeoplePoint(perLoc, { markType, isShowModal: false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 将已去重的实时人员列表同步给轨迹筛选面板,避免面板直接依赖 WebSocket。
|
||||||
|
onPeopleChange?.([...points.current]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { connect, disconnect } = useWebSocket(
|
const { connect, disconnect } = useWebSocket(
|
||||||
|
|
@ -52,6 +54,7 @@ export default function usePeoplePosition(mapMethods, currentBranchOffice) {
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
mapMethods.current && mapMethods.current.removePeoplePointAll();
|
mapMethods.current && mapMethods.current.removePeoplePointAll();
|
||||||
points.current = [];
|
points.current = [];
|
||||||
|
onPeopleChange?.([]);
|
||||||
},
|
},
|
||||||
onMessage: (message) => {
|
onMessage: (message) => {
|
||||||
onMessage(message);
|
onMessage(message);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue