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_MARK = defineNamespace("biMark");
|
||||
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 { 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 (
|
||||
<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">
|
||||
{bottomUtilsMode && (
|
||||
<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,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue