|
@@ -1,19 +1,28 @@
|
|
|
-import type { HotsoptData } from "@/storeDB/hotsopt";
|
|
|
|
|
|
|
+import { useCallback, useState } from "react";
|
|
|
|
|
+import type { HotsoptData, HotsoptItem } from "@/storeDB/hotsopt";
|
|
|
import { useStage } from "@/hook/useStage";
|
|
import { useStage } from "@/hook/useStage";
|
|
|
|
|
+import { getEntDetailApi } from "@/apis";
|
|
|
import { Popover } from "antd";
|
|
import { Popover } from "antd";
|
|
|
import { Mousewheel } from "swiper/modules";
|
|
import { Mousewheel } from "swiper/modules";
|
|
|
import { Swiper, SwiperSlide } from "swiper/react";
|
|
import { Swiper, SwiperSlide } from "swiper/react";
|
|
|
import "swiper/css";
|
|
import "swiper/css";
|
|
|
import style from "./style.module.scss";
|
|
import style from "./style.module.scss";
|
|
|
|
|
+import { DetailModal, type DetailFieldItem } from "@/components/detailModal";
|
|
|
|
|
+import {
|
|
|
|
|
+ buildEntDetailFields,
|
|
|
|
|
+ buildFallbackEntDetail,
|
|
|
|
|
+ formatDetailValue,
|
|
|
|
|
+} from "@/components/detailModal/entDetail";
|
|
|
|
|
+import { useCesiumTilesets, useCesiumViewer } from "@/context/cesiumViewer";
|
|
|
|
|
+import { sitePositionToCartesian } from "@/shared/cesiumOverlay";
|
|
|
|
|
+import { HeadingPitchRange, Matrix4, Math as CesiumMath } from "cesium";
|
|
|
|
|
|
|
|
-const HotSpot = ({
|
|
|
|
|
- icon,
|
|
|
|
|
- name,
|
|
|
|
|
- tag,
|
|
|
|
|
|
|
+const HotSpotList = ({
|
|
|
|
|
+ items,
|
|
|
|
|
+ onSelect,
|
|
|
}: {
|
|
}: {
|
|
|
- icon: string;
|
|
|
|
|
- name: string;
|
|
|
|
|
- tag: string;
|
|
|
|
|
|
|
+ items: HotsoptItem[];
|
|
|
|
|
+ onSelect: (item: HotsoptItem) => void;
|
|
|
}) => {
|
|
}) => {
|
|
|
return (
|
|
return (
|
|
|
<Swiper
|
|
<Swiper
|
|
@@ -25,14 +34,31 @@ const HotSpot = ({
|
|
|
modules={[Mousewheel]}
|
|
modules={[Mousewheel]}
|
|
|
mousewheel
|
|
mousewheel
|
|
|
>
|
|
>
|
|
|
- {Array.from({ length: 10 }).map((_, index) => (
|
|
|
|
|
- <SwiperSlide key={index}>
|
|
|
|
|
|
|
+ {items.map((item) => (
|
|
|
|
|
+ <SwiperSlide key={item.id}>
|
|
|
<div className={style["slide-inner"]}>
|
|
<div className={style["slide-inner"]}>
|
|
|
- <div className={style.hotspot}>
|
|
|
|
|
- <img src={icon} alt="" />
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={style.hotspot}
|
|
|
|
|
+ role="button"
|
|
|
|
|
+ tabIndex={0}
|
|
|
|
|
+ onPointerDown={(ev) => {
|
|
|
|
|
+ ev.stopPropagation();
|
|
|
|
|
+ }}
|
|
|
|
|
+ onClick={(ev) => {
|
|
|
|
|
+ ev.stopPropagation();
|
|
|
|
|
+ onSelect(item);
|
|
|
|
|
+ }}
|
|
|
|
|
+ onKeyDown={(ev) => {
|
|
|
|
|
+ if (ev.key === "Enter" || ev.key === " ") {
|
|
|
|
|
+ ev.preventDefault();
|
|
|
|
|
+ onSelect(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <img src={item.content[0]} alt="" />
|
|
|
<div className="info">
|
|
<div className="info">
|
|
|
- <p>{name}</p>
|
|
|
|
|
- <span>{tag}</span>
|
|
|
|
|
|
|
+ <p>{item.content[1]}</p>
|
|
|
|
|
+ <span>{item.content[2]}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -44,33 +70,106 @@ const HotSpot = ({
|
|
|
|
|
|
|
|
export const Hotsopt = ({ data }: { data: HotsoptData }) => {
|
|
export const Hotsopt = ({ data }: { data: HotsoptData }) => {
|
|
|
const stage = useStage();
|
|
const stage = useStage();
|
|
|
|
|
+ const viewer = useCesiumViewer();
|
|
|
|
|
+ const tilesets = useCesiumTilesets();
|
|
|
|
|
+ const [popoverOpen, setPopoverOpen] = useState(false);
|
|
|
|
|
+ const [detailVisible, setDetailVisible] = useState(false);
|
|
|
|
|
+ const [detailLoading, setDetailLoading] = useState(false);
|
|
|
|
|
+ const [detailTitle, setDetailTitle] = useState("--");
|
|
|
|
|
+ const [detailFields, setDetailFields] = useState<DetailFieldItem[]>([]);
|
|
|
const focused =
|
|
const focused =
|
|
|
stage?.state.focusedHotspotId != null &&
|
|
stage?.state.focusedHotspotId != null &&
|
|
|
stage.state.focusedHotspotId === data.id;
|
|
stage.state.focusedHotspotId === data.id;
|
|
|
- const hasContent = Boolean(data.content);
|
|
|
|
|
|
|
+ const items =
|
|
|
|
|
+ data.items && data.items.length > 0
|
|
|
|
|
+ ? data.items
|
|
|
|
|
+ : data.content
|
|
|
|
|
+ ? [{ id: data.id, content: data.content }]
|
|
|
|
|
+ : [];
|
|
|
|
|
|
|
|
- return (
|
|
|
|
|
- <Popover
|
|
|
|
|
- classNames={{ root: style.popover }}
|
|
|
|
|
- content={
|
|
|
|
|
- hasContent ? (
|
|
|
|
|
- <HotSpot
|
|
|
|
|
- icon={data.content[0]}
|
|
|
|
|
- name={data.content[1]}
|
|
|
|
|
- tag={data.content[2]}
|
|
|
|
|
- />
|
|
|
|
|
- ) : null
|
|
|
|
|
|
|
+ const focusOnMap = useCallback(() => {
|
|
|
|
|
+ if (!stage || !viewer || viewer.isDestroyed()) return;
|
|
|
|
|
+ const tileset = tilesets[data.siteId];
|
|
|
|
|
+ if (!tileset) return;
|
|
|
|
|
+
|
|
|
|
|
+ const target = sitePositionToCartesian(tileset, data.position);
|
|
|
|
|
+ viewer.camera.lookAt(
|
|
|
|
|
+ target,
|
|
|
|
|
+ new HeadingPitchRange(
|
|
|
|
|
+ viewer.camera.heading,
|
|
|
|
|
+ CesiumMath.toRadians(-30),
|
|
|
|
|
+ 80,
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ viewer.camera.lookAtTransform(Matrix4.IDENTITY);
|
|
|
|
|
+ stage.state.setFocusedHotspotId(data.id);
|
|
|
|
|
+ }, [data.id, data.position, data.siteId, stage, tilesets, viewer]);
|
|
|
|
|
+
|
|
|
|
|
+ const openDetail = useCallback(
|
|
|
|
|
+ async (item: HotsoptItem) => {
|
|
|
|
|
+ setPopoverOpen(false);
|
|
|
|
|
+ setDetailVisible(true);
|
|
|
|
|
+ setDetailLoading(true);
|
|
|
|
|
+ setDetailTitle(formatDetailValue(item.content[1]));
|
|
|
|
|
+ setDetailFields([]);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const detail = await getEntDetailApi(item.id);
|
|
|
|
|
+ setDetailTitle(formatDetailValue(detail.name));
|
|
|
|
|
+ setDetailFields(buildEntDetailFields(detail));
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ setDetailFields(
|
|
|
|
|
+ buildEntDetailFields(
|
|
|
|
|
+ buildFallbackEntDetail({
|
|
|
|
|
+ id: item.id,
|
|
|
|
|
+ content: item.content,
|
|
|
|
|
+ siteId: data.siteId,
|
|
|
|
|
+ }),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setDetailLoading(false);
|
|
|
}
|
|
}
|
|
|
- placement="right"
|
|
|
|
|
- arrow={false}
|
|
|
|
|
- destroyOnHidden={true}
|
|
|
|
|
- >
|
|
|
|
|
- <div
|
|
|
|
|
- className={`${style["hotsopt-flat"]} ${style[data.className ?? ""]} ${focused ? style["hotsopt-focused"] : ""}`}
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ [data.siteId],
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Popover
|
|
|
|
|
+ classNames={{ root: style.popover }}
|
|
|
|
|
+ open={popoverOpen}
|
|
|
|
|
+ onOpenChange={setPopoverOpen}
|
|
|
|
|
+ content={
|
|
|
|
|
+ items.length > 0 ? (
|
|
|
|
|
+ <HotSpotList items={items} onSelect={openDetail} />
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ }
|
|
|
|
|
+ placement="right"
|
|
|
|
|
+ arrow={false}
|
|
|
|
|
+ destroyOnHidden={true}
|
|
|
>
|
|
>
|
|
|
- {data.num && <p className="num">【{data.num}】</p>}
|
|
|
|
|
- </div>
|
|
|
|
|
- </Popover>
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`${style["hotsopt-flat"]} ${style[data.className ?? ""]} ${focused ? style["hotsopt-focused"] : ""}`}
|
|
|
|
|
+ onPointerDown={(ev) => {
|
|
|
|
|
+ ev.stopPropagation();
|
|
|
|
|
+ focusOnMap();
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {data.num != null && data.num > 1 && (
|
|
|
|
|
+ <p className="num">【{data.num}】</p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Popover>
|
|
|
|
|
+
|
|
|
|
|
+ <DetailModal
|
|
|
|
|
+ open={detailVisible}
|
|
|
|
|
+ loading={detailLoading}
|
|
|
|
|
+ title={detailTitle}
|
|
|
|
|
+ fields={detailFields}
|
|
|
|
|
+ onCancel={() => setDetailVisible(false)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|