|
|
@@ -0,0 +1,622 @@
|
|
|
+import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
|
+import styles from './index.module.scss'
|
|
|
+import { backPageFu } from '@/utils/history'
|
|
|
+import worldSvg from '@/assets/svg/world.svg'
|
|
|
+import hotL from '@/assets/img/hot_l.png'
|
|
|
+import hotR from '@/assets/img/hot_r.png'
|
|
|
+import link1 from '@/assets/svg/Links/1.png'
|
|
|
+import link2 from '@/assets/svg/Links/2.png'
|
|
|
+import link3 from '@/assets/svg/Links/3.png'
|
|
|
+import link4 from '@/assets/svg/Links/4.png'
|
|
|
+import link5 from '@/assets/svg/Links/5.png'
|
|
|
+import link6 from '@/assets/svg/Links/6.png'
|
|
|
+import linkAc1 from '@/assets/svg/LinksAc/1.png'
|
|
|
+import linkAc2 from '@/assets/svg/LinksAc/2.png'
|
|
|
+import linkAc3 from '@/assets/svg/LinksAc/3.png'
|
|
|
+import linkAc4 from '@/assets/svg/LinksAc/4.png'
|
|
|
+import linkAc5 from '@/assets/svg/LinksAc/5.png'
|
|
|
+import linkAc6 from '@/assets/svg/LinksAc/6.png'
|
|
|
+import Hots, { DetailBack } from './components/Hots'
|
|
|
+import Wenwu from './components/Wenwu'
|
|
|
+
|
|
|
+/** 横向拼接份数:左右各一份缓冲,循环时不会露白 */
|
|
|
+const MAP_COPIES = 3
|
|
|
+const DRAG_THRESHOLD = 8
|
|
|
+/** 地图初始水平偏移,对应 translate3d(-361px, -2%, 0) */
|
|
|
+const INITIAL_TRANSLATE_X = -400
|
|
|
+/** 贴图左右各内缩,避免大洋空白被当成陆地 */
|
|
|
+const CONTINENT_INSET = 0.12
|
|
|
+/** 已选中的洲边界放宽,减少边缘抖动 */
|
|
|
+const CONTINENT_STICKY_INSET = 0.06
|
|
|
+/** 高亮与热点的展示顺序 */
|
|
|
+const CONTINENT_ORDER = ['美洲', '欧洲', '非洲', '亚洲', '大洋洲']
|
|
|
+
|
|
|
+const LINK_SRC: Record<string, string> = {
|
|
|
+ '1': link1,
|
|
|
+ '2': link2,
|
|
|
+ '3': link3,
|
|
|
+ '4': link4,
|
|
|
+ '5': link5,
|
|
|
+ '6': link6
|
|
|
+}
|
|
|
+
|
|
|
+const LINK_AC_SRC: Record<string, string> = {
|
|
|
+ '1': linkAc1,
|
|
|
+ '2': linkAc2,
|
|
|
+ '3': linkAc3,
|
|
|
+ '4': linkAc4,
|
|
|
+ '5': linkAc5,
|
|
|
+ '6': linkAc6
|
|
|
+}
|
|
|
+
|
|
|
+/** 卫星贴图序号对应展区大洲 */
|
|
|
+const LINK_CONTINENT: Record<string, string> = {
|
|
|
+ '1': '美洲',
|
|
|
+ '2': '美洲',
|
|
|
+ '3': '欧洲',
|
|
|
+ '4': '亚洲',
|
|
|
+ '5': '非洲',
|
|
|
+ '6': '大洋洲'
|
|
|
+}
|
|
|
+
|
|
|
+type Hotspot = {
|
|
|
+ id: string
|
|
|
+ name: string
|
|
|
+ title: string
|
|
|
+ continent: string
|
|
|
+ x: number
|
|
|
+ y: number
|
|
|
+ side: 'l' | 'r'
|
|
|
+ /** 三维光柱在左 l / 在右 r,默认 l */
|
|
|
+ hotSide?: 'l' | 'r'
|
|
|
+ /** 三维标签在上 t / 在下 b,默认 t */
|
|
|
+ hotY?: 't' | 'b'
|
|
|
+ hotLeft?: number
|
|
|
+ hotRight?: number
|
|
|
+ hotTop?: number
|
|
|
+ hotBottom?: number
|
|
|
+ intro: string
|
|
|
+ timeline: { year: string; text: string }[]
|
|
|
+ videoSrc?: string
|
|
|
+ imgs?: string[]
|
|
|
+ relic?: RelicInfoType
|
|
|
+}
|
|
|
+
|
|
|
+/** 静态资源路径:兼容相对路径与完整 URL */
|
|
|
+function toAssetUrl(src?: string) {
|
|
|
+ if (!src) return undefined
|
|
|
+ if (/^(https?:)?\/\//i.test(src) || src.startsWith('data:')) return src
|
|
|
+ const base = (process.env.PUBLIC_URL || '.').replace(/\/$/, '')
|
|
|
+ return `${base}/${src.replace(/^\//, '')}`
|
|
|
+}
|
|
|
+
|
|
|
+/** 从 infoTemp 展区城市生成地图热点 */
|
|
|
+function buildHotspots(): Hotspot[] {
|
|
|
+ return Object.values(infoTemp.sectionsInfo).flatMap(section =>
|
|
|
+ (section.citys || []).map(city => ({
|
|
|
+ id: city.id,
|
|
|
+ name: city.shortName || city.name,
|
|
|
+ title: city.title || city.name,
|
|
|
+ continent: section.title,
|
|
|
+ x: city.x,
|
|
|
+ y: city.y,
|
|
|
+ side: city.side,
|
|
|
+ hotSide: city.hotSide,
|
|
|
+ hotY: city.hotY,
|
|
|
+ hotLeft: city.hotLeft,
|
|
|
+ hotRight: city.hotRight,
|
|
|
+ hotTop: city.hotTop,
|
|
|
+ hotBottom: city.hotBottom,
|
|
|
+ intro: city.desc,
|
|
|
+ timeline: (city.times || []).map(item => ({
|
|
|
+ year: item.year,
|
|
|
+ text: item.content
|
|
|
+ })),
|
|
|
+ videoSrc: city.videoSrc,
|
|
|
+ imgs: (city.imgs || []).filter(Boolean),
|
|
|
+ relic:
|
|
|
+ city.relic && typeof city.relic === 'object' && city.relic.src
|
|
|
+ ? city.relic
|
|
|
+ : undefined
|
|
|
+ }))
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const HOTSPOTS = buildHotspots()
|
|
|
+
|
|
|
+/** 给每份地图的 id 加后缀,避免三份 SVG 的 id 冲突 */
|
|
|
+function suffixSvgIds(svg: string, suffix: string) {
|
|
|
+ return svg.replace(/\sid="([^"]+)"/g, ` id="$1${suffix}"`)
|
|
|
+}
|
|
|
+
|
|
|
+/** 将偏移折回到 [0, width) */
|
|
|
+function wrapOffset(offset: number, width: number) {
|
|
|
+ if (width <= 0) return 0
|
|
|
+ let x = offset % width
|
|
|
+ if (x < 0) x += width
|
|
|
+ return x
|
|
|
+}
|
|
|
+
|
|
|
+function A2layout() {
|
|
|
+ const [worldHtml, setWorldHtml] = useState('')
|
|
|
+ const [expanded, setExpanded] = useState(false)
|
|
|
+ const [mapDragging, setMapDragging] = useState(false)
|
|
|
+ const [activeCity, setActiveCity] = useState<Hotspot | null>(null)
|
|
|
+ /** 视频播完后展台、热点再渐显 */
|
|
|
+ const [overlayShow, setOverlayShow] = useState(false)
|
|
|
+ /** 详情正文是否已滚离顶部,用于降低返回按钮透明度 */
|
|
|
+ const [cardScrolled, setCardScrolled] = useState(false)
|
|
|
+ /** 屏幕水平中心当前经过的大洲,可重叠 */
|
|
|
+ const [centerContinents, setCenterContinents] = useState<string[]>([])
|
|
|
+ const centerContinentsRef = useRef<string[]>([])
|
|
|
+ const shownCityRef = useRef<Hotspot | null>(null)
|
|
|
+ if (activeCity) shownCityRef.current = activeCity
|
|
|
+ const shownCity = activeCity ?? shownCityRef.current
|
|
|
+ const drawerRef = useRef<HTMLDivElement>(null)
|
|
|
+ const startY = useRef(0)
|
|
|
+ const dragging = useRef(false)
|
|
|
+ const pointerDown = useRef(false)
|
|
|
+
|
|
|
+ const mapAreaRef = useRef<HTMLDivElement>(null)
|
|
|
+ const trackRef = useRef<HTMLDivElement>(null)
|
|
|
+ const copyRef = useRef<HTMLDivElement>(null)
|
|
|
+ const copyWidthRef = useRef(0)
|
|
|
+ const panRef = useRef(0)
|
|
|
+ const panInitedRef = useRef(false)
|
|
|
+ const mapPointerId = useRef<number | null>(null)
|
|
|
+ const mapStartX = useRef(0)
|
|
|
+ const mapStartPan = useRef(0)
|
|
|
+ const mapMoved = useRef(false)
|
|
|
+ const videoRef = useRef<HTMLVideoElement>(null)
|
|
|
+ const detailBodyRef = useRef<HTMLDivElement>(null)
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ let cancelled = false
|
|
|
+ fetch(worldSvg)
|
|
|
+ .then(res => res.text())
|
|
|
+ .then(text => {
|
|
|
+ if (cancelled) return
|
|
|
+ const html = text
|
|
|
+ .replace(/<\?xml[^>]*\?>/, '')
|
|
|
+ .replace(/viewBox="0 0 2000 857"/, 'viewBox="60 0 1786 857"')
|
|
|
+ .replace(
|
|
|
+ /<image([^>]*?)xlink:href="Links\/(\d)\.png"([^>]*)>\s*<\/image>/g,
|
|
|
+ (_, before: string, n: string, after: string) => {
|
|
|
+ const continent = LINK_CONTINENT[n]
|
|
|
+ return (
|
|
|
+ `<image${before}xlink:href="${LINK_SRC[n]}" data-continent="${continent}"${after}></image>` +
|
|
|
+ `<image${before}xlink:href="${LINK_AC_SRC[n]}" data-continent-ac="${continent}"${after}></image>`
|
|
|
+ )
|
|
|
+ }
|
|
|
+ )
|
|
|
+ setWorldHtml(html)
|
|
|
+ })
|
|
|
+ return () => {
|
|
|
+ cancelled = true
|
|
|
+ }
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const applyTrackTransform = useCallback(() => {
|
|
|
+ const track = trackRef.current
|
|
|
+ const area = mapAreaRef.current
|
|
|
+ const width = copyWidthRef.current
|
|
|
+ if (!track || !area || width <= 0) return
|
|
|
+ const align = (area.clientWidth - width) / 2
|
|
|
+ if (!panInitedRef.current) {
|
|
|
+ panRef.current = wrapOffset(INITIAL_TRANSLATE_X - align + width, width)
|
|
|
+ panInitedRef.current = true
|
|
|
+ } else {
|
|
|
+ panRef.current = wrapOffset(panRef.current, width)
|
|
|
+ }
|
|
|
+ track.style.transform = `translate3d(${align - width + panRef.current}px, -2%, 0)`
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ /** 当前经过的洲叠上 LinksAc 高亮贴图 */
|
|
|
+ const applyContinentHighlight = useCallback((names: string[]) => {
|
|
|
+ const track = trackRef.current
|
|
|
+ if (!track) return
|
|
|
+ const on = new Set(names)
|
|
|
+ track.querySelectorAll<SVGImageElement>('image[data-continent-ac]').forEach(img => {
|
|
|
+ const name = img.getAttribute('data-continent-ac')
|
|
|
+ img.classList.toggle('isOn', Boolean(name && on.has(name)))
|
|
|
+ })
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ /** 屏幕水平中心经过哪些洲(可重叠,如欧非) */
|
|
|
+ const updateCenterContinent = useCallback(() => {
|
|
|
+ const area = mapAreaRef.current
|
|
|
+ const track = trackRef.current
|
|
|
+ if (!area || !track) return
|
|
|
+ const midX = area.getBoundingClientRect().left + area.clientWidth / 2
|
|
|
+ const hits = new Set<string>()
|
|
|
+ track.querySelectorAll<SVGImageElement>('image[data-continent]').forEach(img => {
|
|
|
+ const name = img.getAttribute('data-continent')
|
|
|
+ if (!name) return
|
|
|
+ const rect = img.getBoundingClientRect()
|
|
|
+ if (rect.width <= 0) return
|
|
|
+ const sticky = centerContinentsRef.current.includes(name)
|
|
|
+ const inset = rect.width * (sticky ? CONTINENT_STICKY_INSET : CONTINENT_INSET)
|
|
|
+ if (midX < rect.left + inset || midX > rect.right - inset) return
|
|
|
+ hits.add(name)
|
|
|
+ })
|
|
|
+ const next = CONTINENT_ORDER.filter(name => hits.has(name))
|
|
|
+ const prev = centerContinentsRef.current
|
|
|
+ if (next.length === prev.length && next.every((name, i) => name === prev[i])) return
|
|
|
+ centerContinentsRef.current = next
|
|
|
+ applyContinentHighlight(next)
|
|
|
+ setCenterContinents(next)
|
|
|
+ }, [applyContinentHighlight])
|
|
|
+
|
|
|
+ const measureAndApply = useCallback(() => {
|
|
|
+ const width = copyRef.current?.offsetWidth ?? 0
|
|
|
+ if (width > 0) copyWidthRef.current = width
|
|
|
+ applyTrackTransform()
|
|
|
+ updateCenterContinent()
|
|
|
+ }, [applyTrackTransform, updateCenterContinent])
|
|
|
+
|
|
|
+ useLayoutEffect(() => {
|
|
|
+ if (!worldHtml) return
|
|
|
+ measureAndApply()
|
|
|
+ const area = mapAreaRef.current
|
|
|
+ const copy = copyRef.current
|
|
|
+ if (!area || !copy) return
|
|
|
+ const observer = new ResizeObserver(() => measureAndApply())
|
|
|
+ observer.observe(area)
|
|
|
+ observer.observe(copy)
|
|
|
+ return () => observer.disconnect()
|
|
|
+ }, [worldHtml, measureAndApply])
|
|
|
+
|
|
|
+ const onMapPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
|
|
+ if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
|
+ mapPointerId.current = e.pointerId
|
|
|
+ mapStartX.current = e.clientX
|
|
|
+ mapStartPan.current = panRef.current
|
|
|
+ mapMoved.current = false
|
|
|
+ e.currentTarget.setPointerCapture(e.pointerId)
|
|
|
+ setMapDragging(true)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const onMapPointerMove = useCallback(
|
|
|
+ (e: React.PointerEvent<HTMLDivElement>) => {
|
|
|
+ if (mapPointerId.current !== e.pointerId) return
|
|
|
+ const dx = e.clientX - mapStartX.current
|
|
|
+ if (Math.abs(dx) > DRAG_THRESHOLD) mapMoved.current = true
|
|
|
+ panRef.current = mapStartPan.current + dx
|
|
|
+ applyTrackTransform()
|
|
|
+ updateCenterContinent()
|
|
|
+ },
|
|
|
+ [applyTrackTransform, updateCenterContinent]
|
|
|
+ )
|
|
|
+
|
|
|
+ const endMapPointer = useCallback(
|
|
|
+ (e: React.PointerEvent<HTMLDivElement>) => {
|
|
|
+ if (mapPointerId.current !== e.pointerId) return
|
|
|
+ mapPointerId.current = null
|
|
|
+ applyTrackTransform()
|
|
|
+ updateCenterContinent()
|
|
|
+ setMapDragging(false)
|
|
|
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
|
+ e.currentTarget.releasePointerCapture(e.pointerId)
|
|
|
+ }
|
|
|
+ // 拖拽收尾的 click 仍拦截;等 click 过后再清标记,否则热点再也点不进
|
|
|
+ if (mapMoved.current) {
|
|
|
+ window.setTimeout(() => {
|
|
|
+ mapMoved.current = false
|
|
|
+ }, 0)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [applyTrackTransform, updateCenterContinent]
|
|
|
+ )
|
|
|
+
|
|
|
+ const onHotspotClick = useCallback((id: string) => {
|
|
|
+ if (mapMoved.current) return
|
|
|
+ const city = HOTSPOTS.find(item => item.id === id)
|
|
|
+ if (!city) return
|
|
|
+ setExpanded(false)
|
|
|
+ setActiveCity(city)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ setCardScrolled(false)
|
|
|
+ if (detailBodyRef.current) detailBodyRef.current.scrollTop = 0
|
|
|
+ }, [activeCity?.id])
|
|
|
+
|
|
|
+ const onDetailScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
|
+ setCardScrolled(e.currentTarget.scrollTop > 8)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!activeCity) {
|
|
|
+ videoRef.current?.pause()
|
|
|
+ setOverlayShow(false)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!activeCity.videoSrc) {
|
|
|
+ setOverlayShow(true)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const video = videoRef.current
|
|
|
+ if (!video) {
|
|
|
+ setOverlayShow(true)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ video.muted = true
|
|
|
+ setOverlayShow(false)
|
|
|
+ const onEnded = () => setOverlayShow(true)
|
|
|
+ const onError = () => setOverlayShow(true)
|
|
|
+ video.addEventListener('ended', onEnded)
|
|
|
+ video.addEventListener('error', onError)
|
|
|
+ video.currentTime = 0
|
|
|
+ const play = video.play()
|
|
|
+ if (play) play.catch(() => setOverlayShow(true))
|
|
|
+ return () => {
|
|
|
+ video.removeEventListener('ended', onEnded)
|
|
|
+ video.removeEventListener('error', onError)
|
|
|
+ }
|
|
|
+ }, [activeCity])
|
|
|
+
|
|
|
+ const onPointerDown = useCallback((e: React.PointerEvent) => {
|
|
|
+ pointerDown.current = true
|
|
|
+ dragging.current = false
|
|
|
+ startY.current = e.clientY
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const onPointerMove = useCallback((e: React.PointerEvent) => {
|
|
|
+ if (!pointerDown.current) return
|
|
|
+ if (Math.abs(e.clientY - startY.current) > 8) {
|
|
|
+ dragging.current = true
|
|
|
+ }
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const onPointerUp = useCallback((e: React.PointerEvent) => {
|
|
|
+ if (!pointerDown.current) return
|
|
|
+ pointerDown.current = false
|
|
|
+ const dy = e.clientY - startY.current
|
|
|
+ if (dy < -40) {
|
|
|
+ setExpanded(true)
|
|
|
+ } else if (dy > 40) {
|
|
|
+ setExpanded(false)
|
|
|
+ }
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const onClickDrawer = useCallback(() => {
|
|
|
+ if (dragging.current) return
|
|
|
+ if (!expanded) setExpanded(true)
|
|
|
+ }, [expanded])
|
|
|
+
|
|
|
+ // 展开后把焦点放在抽屉上,便于失焦时收起
|
|
|
+ useEffect(() => {
|
|
|
+ if (!expanded) return
|
|
|
+ drawerRef.current?.focus({ preventScroll: true })
|
|
|
+ }, [expanded])
|
|
|
+
|
|
|
+ const onBlurDrawer = useCallback((e: React.FocusEvent<HTMLDivElement>) => {
|
|
|
+ const next = e.relatedTarget as Node | null
|
|
|
+ if (next && drawerRef.current?.contains(next)) return
|
|
|
+ setExpanded(false)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ const onPagePointerDown = useCallback(
|
|
|
+ (e: React.PointerEvent) => {
|
|
|
+ if (!expanded) return
|
|
|
+ if (drawerRef.current?.contains(e.target as Node)) return
|
|
|
+ setExpanded(false)
|
|
|
+ },
|
|
|
+ [expanded]
|
|
|
+ )
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className={styles.A2layout} onPointerDown={onPagePointerDown}>
|
|
|
+ <div
|
|
|
+ ref={mapAreaRef}
|
|
|
+ className={`${styles.mapArea} ${mapDragging ? styles.mapDragging : ''}`}
|
|
|
+ onPointerDown={onMapPointerDown}
|
|
|
+ onPointerMove={onMapPointerMove}
|
|
|
+ onPointerUp={endMapPointer}
|
|
|
+ onPointerCancel={endMapPointer}
|
|
|
+ >
|
|
|
+ <div ref={trackRef} className={styles.world}>
|
|
|
+ {worldHtml
|
|
|
+ ? Array.from({ length: MAP_COPIES }, (_, i) => (
|
|
|
+ <div
|
|
|
+ key={i}
|
|
|
+ ref={i === 1 ? copyRef : undefined}
|
|
|
+ className={styles.mapCopy}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className={styles.mapSvg}
|
|
|
+ dangerouslySetInnerHTML={{
|
|
|
+ __html: suffixSvgIds(worldHtml, `__c${i}`)
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ {HOTSPOTS.map(hot => (
|
|
|
+ <button
|
|
|
+ key={hot.id}
|
|
|
+ type="button"
|
|
|
+ className={`${styles.hotspot} ${
|
|
|
+ hot.side === 'l' ? styles.hotL : styles.hotR
|
|
|
+ } ${
|
|
|
+ centerContinents.includes(hot.continent) ? '' : styles.hotspotOff
|
|
|
+ }`}
|
|
|
+ style={{ left: `${hot.x * 100}%`, top: `${hot.y * 100}%` }}
|
|
|
+ data-hotspot={hot.id}
|
|
|
+ onPointerDown={e => {
|
|
|
+ e.stopPropagation()
|
|
|
+ // 热点自己接收点按,清掉地图拖拽标记,避免拖完再也进不了详情
|
|
|
+ mapMoved.current = false
|
|
|
+ }}
|
|
|
+ onClick={() => onHotspotClick(hot.id)}
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ draggable={false}
|
|
|
+ src={hot.side === 'l' ? hotL : hotR}
|
|
|
+ alt=""
|
|
|
+ />
|
|
|
+ <span>{hot.name}</span>
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ))
|
|
|
+ : null}
|
|
|
+ </div>
|
|
|
+ <p className={styles.hint}>
|
|
|
+ 点击地图,探访友谊之城
|
|
|
+ <span className={styles.hand} aria-hidden>
|
|
|
+ <img draggable={false} src={require('@/assets/img/gesture.png')} alt="hand" />
|
|
|
+ </span>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ ref={drawerRef}
|
|
|
+ tabIndex={-1}
|
|
|
+ className={`${styles.drawer} ${expanded ? styles.expanded : ''}`}
|
|
|
+ onBlur={onBlurDrawer}
|
|
|
+ onClick={onClickDrawer}
|
|
|
+ {...(!expanded
|
|
|
+ ? {
|
|
|
+ onPointerDown,
|
|
|
+ onPointerMove,
|
|
|
+ onPointerUp,
|
|
|
+ onPointerCancel: onPointerUp
|
|
|
+ }
|
|
|
+ : {})}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className={styles.handleWrap}
|
|
|
+ {...(expanded
|
|
|
+ ? {
|
|
|
+ onPointerDown,
|
|
|
+ onPointerMove,
|
|
|
+ onPointerUp,
|
|
|
+ onPointerCancel: onPointerUp
|
|
|
+ }
|
|
|
+ : {})}
|
|
|
+ >
|
|
|
+ <span className={styles.handle} />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ className={styles.body}
|
|
|
+ dangerouslySetInnerHTML={{ __html: infoTemp.introInfo }}
|
|
|
+ />
|
|
|
+ <div className={styles.footer}>
|
|
|
+ {expanded ? (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={`${styles.actionBtn} ${styles.btnShouqi}`}
|
|
|
+ aria-label="收起"
|
|
|
+ onClick={e => {
|
|
|
+ e.stopPropagation()
|
|
|
+ setExpanded(false)
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={`${styles.actionBtn} ${styles.btnVr}`}
|
|
|
+ aria-label="VR观展"
|
|
|
+ onClick={e => {
|
|
|
+ e.stopPropagation()
|
|
|
+ if (!infoTemp.sceneCode) return
|
|
|
+ window.location.href = `https://sit-supertwocustom.4dage.com/${infoTemp.sceneCode}/index.html`
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {!expanded ? (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className={`${styles.actionBtn} ${styles.btnBack}`}
|
|
|
+ aria-label="返回"
|
|
|
+ onClick={e => {
|
|
|
+ e.stopPropagation()
|
|
|
+ backPageFu('/')
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ className={`${styles.detail} ${activeCity ? styles.detailShow : ''}`}
|
|
|
+ aria-hidden={!activeCity}
|
|
|
+ >
|
|
|
+ {shownCity ? (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className={`${styles.detailCard} ${
|
|
|
+ shownCity.videoSrc ? '' : styles.detailCardFull
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ ref={detailBodyRef}
|
|
|
+ className={styles.detailBody}
|
|
|
+ onScroll={onDetailScroll}
|
|
|
+ >
|
|
|
+ <p className={styles.detailIntro}>{shownCity.intro}</p>
|
|
|
+ {shownCity.imgs && shownCity.imgs.length > 0 ? (
|
|
|
+ <div className={styles.detailPhotos}>
|
|
|
+ {shownCity.imgs.map((src, i) => (
|
|
|
+ <img key={`${src}-${i}`} src={toAssetUrl(src)} alt="" />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ <ul className={styles.timeline}>
|
|
|
+ {shownCity.timeline.map((item, i) => (
|
|
|
+ <li key={`${item.year}-${i}`} className={styles.timelineItem}>
|
|
|
+ <i className={styles.timelineDot} />
|
|
|
+ <span className={styles.timelineYear}>{item.year}</span>
|
|
|
+ <p>{item.text}</p>
|
|
|
+ </li>
|
|
|
+ ))}
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {shownCity.videoSrc ? (
|
|
|
+ <div className={styles.detailLower}>
|
|
|
+ <video
|
|
|
+ key={shownCity.id}
|
|
|
+ ref={videoRef}
|
|
|
+ src={toAssetUrl(shownCity.videoSrc)}
|
|
|
+ autoPlay
|
|
|
+ muted
|
|
|
+ playsInline
|
|
|
+ preload="auto"
|
|
|
+ />
|
|
|
+ <Hots
|
|
|
+ name={shownCity.title}
|
|
|
+ visible={overlayShow}
|
|
|
+ backDimmed={cardScrolled}
|
|
|
+ onBack={() => setActiveCity(null)}
|
|
|
+ side={shownCity.hotSide ?? 'l'}
|
|
|
+ align={shownCity.hotY ?? 't'}
|
|
|
+ left={shownCity.hotLeft}
|
|
|
+ right={shownCity.hotRight}
|
|
|
+ top={shownCity.hotTop}
|
|
|
+ bottom={shownCity.hotBottom}
|
|
|
+ />
|
|
|
+ <Wenwu
|
|
|
+ relic={
|
|
|
+ shownCity.relic
|
|
|
+ ? { ...shownCity.relic, src: toAssetUrl(shownCity.relic.src) || '' }
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ visible={overlayShow}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <DetailBack
|
|
|
+ visible={overlayShow}
|
|
|
+ dimmed={cardScrolled}
|
|
|
+ onBack={() => setActiveCity(null)}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const MemoA2layout = React.memo(A2layout)
|
|
|
+
|
|
|
+export default MemoA2layout
|