diff --git a/src/api/biPeople/index.js b/src/api/biPeople/index.js
new file mode 100644
index 0000000..abc1b47
--- /dev/null
+++ b/src/api/biPeople/index.js
@@ -0,0 +1,7 @@
+import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
+
+// 人员轨迹按终端或人员编号、时间范围查询,接口参数由轨迹筛选面板传入。
+export const getPeopleTrajectory = declareRequest(
+ "getPeopleTrajectoryLoading",
+ "Post > @/personnelPosition/bi/personTrace",
+);
diff --git a/src/enumerate/namespace/index.js b/src/enumerate/namespace/index.js
index a309dde..80113a4 100644
--- a/src/enumerate/namespace/index.js
+++ b/src/enumerate/namespace/index.js
@@ -9,3 +9,4 @@ export const NS_GLOBAL = defineNamespace("global");
export const NS_BI_STATISTICS = defineNamespace("biStatistics");
export const NS_BI_MARK = defineNamespace("biMark");
export const NS_BI_MARK_INFO = defineNamespace("biMarkInfo");
+export const NS_BI_PEOPLE = defineNamespace("biPeople");
diff --git a/src/pages/Container/Map/components/BottomUtils/index.js b/src/pages/Container/Map/components/BottomUtils/index.js
index 8cd2770..8fc1515 100644
--- a/src/pages/Container/Map/components/BottomUtils/index.js
+++ b/src/pages/Container/Map/components/BottomUtils/index.js
@@ -11,6 +11,7 @@ import { NS_BI_MARK } from "~/enumerate/namespace";
import { branchOfficeUtilsList } from "~/pages/Container/Map/components/BottomUtils/branchOfficeUtilsList";
import { Context } from "~/pages/Container/Map/js/context";
import usePeoplePosition from "~/pages/Container/Map/js/usePeoplePosition";
+import PeopleTrajectorySelect from "~/pages/Container/Map/components/PeopleTrajectorySelect";
import { portUtilsList } from "./portUtilsList";
import { usePortUtilsAnimation } from "./usePortUtilsAnimation";
import "./index.less";
@@ -67,7 +68,12 @@ function BottomUtils(props) {
portArea,
} = 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
? "port"
@@ -92,6 +98,10 @@ function BottomUtils(props) {
);
const resetAllCheck = () => {
+ // 底部工具栏被外部操作重置时,人员定位的 WebSocket 和 Cesium 实体不会随勾选状态自动销毁,需要统一清理。
+ peoplePositionDisconnect();
+ mapMethods.current.removePeoplePointAll();
+ setPeopleList([]);
setList(produce((draft) => {
draft.forEach((item) => {
item.list.forEach((item1) => {
@@ -181,6 +191,7 @@ function BottomUtils(props) {
if (!check) {
if (item.type === "peoplePosition") {
resetPeopleTrajectory(index);
+ mapMethods.current.removePeopleTrajectory();
peoplePositionDisconnect();
return;
}
@@ -209,6 +220,8 @@ function BottomUtils(props) {
await addMarkPoint(item);
};
+ const isPeopleTrajectoryChecked = list.some(item => item.list?.some(option => option.type === "peopleTrajectory" && option.check));
+
const renderPortUtils = () => {
if (bottomUtilsMode === "port" && listMode === bottomUtilsMode) {
return list.map((item, index) => {
@@ -313,6 +326,18 @@ function BottomUtils(props) {
return (
+
+ {isPeopleTrajectoryChecked && (
+
+
+
+ )}
+
{bottomUtilsMode && (
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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Connect([NS_BI_PEOPLE], true)(PeopleTrajectorySelect);
diff --git a/src/pages/Container/Map/components/PeopleTrajectorySelect/index.less b/src/pages/Container/Map/components/PeopleTrajectorySelect/index.less
new file mode 100644
index 0000000..946b304
--- /dev/null
+++ b/src/pages/Container/Map/components/PeopleTrajectorySelect/index.less
@@ -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;
+ }
+}
diff --git a/src/pages/Container/Map/js/useMapMethods.js b/src/pages/Container/Map/js/useMapMethods.js
index 5348093..453039c 100644
--- a/src/pages/Container/Map/js/useMapMethods.js
+++ b/src/pages/Container/Map/js/useMapMethods.js
@@ -63,6 +63,7 @@ const peopleImg = {
red: people_red,
yellow: people_yellow,
}; // 人员定位颜色图片
+const peopleTrajectoryEntityId = "peopleTrajectoryEntity";
const Cesium = window.Cesium;
@@ -410,6 +411,29 @@ export default function useMapMethods(viewerRef, request) {
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 的全局时钟;每次开启定位仅初始化一次,避免首批点位互相重置时钟。
const initPeoplePositionClock = () => {
const start = Cesium.JulianDate.now();
@@ -492,6 +516,7 @@ export default function useMapMethods(viewerRef, request) {
});
peopleId.current = [];
peoplePositionStartTime.current = null;
+ removePeopleTrajectory();
};
return {
@@ -509,6 +534,8 @@ export default function useMapMethods(viewerRef, request) {
removeWall,
addMarkPoint,
removeMarkPoint,
+ addPeopleTrajectory,
+ removePeopleTrajectory,
initPeoplePositionClock,
addPeoplePoint,
updatePeoplePoint,
diff --git a/src/pages/Container/Map/js/usePeoplePosition.js b/src/pages/Container/Map/js/usePeoplePosition.js
index 16febba..4ce5797 100644
--- a/src/pages/Container/Map/js/usePeoplePosition.js
+++ b/src/pages/Container/Map/js/usePeoplePosition.js
@@ -3,7 +3,7 @@ import { useRef } from "react";
import edgeQHD from "./edge/qhd.js";
import { filterNull, isPointWithinTheArea } from "./utils";
-export default function usePeoplePosition(mapMethods, currentBranchOffice) {
+export default function usePeoplePosition(mapMethods, currentBranchOffice, onPeopleChange) {
const markType = "peoplePosition";
const points = useRef([]);
@@ -43,6 +43,8 @@ export default function usePeoplePosition(mapMethods, currentBranchOffice) {
mapMethods.current && (await mapMethods.current.addPeoplePoint(perLoc, { markType, isShowModal: false }));
}
}
+ // 将已去重的实时人员列表同步给轨迹筛选面板,避免面板直接依赖 WebSocket。
+ onPeopleChange?.([...points.current]);
};
const { connect, disconnect } = useWebSocket(
@@ -52,6 +54,7 @@ export default function usePeoplePosition(mapMethods, currentBranchOffice) {
onClose: () => {
mapMethods.current && mapMethods.current.removePeoplePointAll();
points.current = [];
+ onPeopleChange?.([]);
},
onMessage: (message) => {
onMessage(message);