This commit is contained in:
Daniel
2026-03-04 16:48:17 +08:00
parent 64f4c438c3
commit 26938449f0
34 changed files with 956 additions and 500 deletions

View File

@@ -4,17 +4,8 @@ import type { MapRef } from 'react-map-gl'
import type { Map as MapboxMap } from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { useReplaySituation } from '@/hooks/useReplaySituation'
import { usePlaybackStore } from '@/store/playbackStore'
import { config } from '@/config'
import {
ATTACKED_TARGETS,
ALLIED_STRIKE_LOCATIONS,
LINCOLN_COORDS,
LINCOLN_STRIKE_TARGETS,
FORD_COORDS,
FORD_STRIKE_TARGETS,
ISRAEL_STRIKE_SOURCE,
ISRAEL_STRIKE_TARGETS,
} from '@/data/mapLocations'
import { EXTENDED_WAR_ZONES } from '@/data/extendedWarData'
const MAPBOX_TOKEN = config.mapboxAccessToken || ''
@@ -65,8 +56,45 @@ const ALLIES_ADMIN = [
// 伊朗攻击源 德黑兰 [lng, lat]
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
// 真主党打击源(黎巴嫩南部大致位置),用于绘制向以色列北部的攻击矢量
const HEZBOLLAH_SOURCE: [number, number] = [35.3, 33.2]
// API 未返回 mapData 时的静态 fallback保证美/以打击线与动画不消失(与 server/seed.js 一致)
const FALLBACK_STRIKE_SOURCES: { id: string; name: string; lng: number; lat: number }[] = [
{ id: 'israel', name: '以色列', lng: 34.78, lat: 32.08 },
{ id: 'lincoln', name: '林肯号航母', lng: 58.4215, lat: 24.1568 },
{ id: 'ford', name: '福特号航母', lng: 24.1002, lat: 35.7397 },
]
const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: number; name?: string }[] }[] = [
{
sourceId: 'israel',
targets: [
{ lng: 50.88, lat: 34.64, name: '库姆' },
{ lng: 50.876409, lat: 34.625448, name: '伊朗专家会议秘书处' },
{ lng: 51.916, lat: 33.666, name: '纳坦兹' },
{ lng: 51.002, lat: 35.808, name: '卡拉季无人机厂' },
],
},
{
sourceId: 'lincoln',
targets: [
{ lng: 56.27, lat: 27.18, name: '阿巴斯港海军司令部' },
{ lng: 57.08, lat: 27.13, name: '米纳布' },
{ lng: 56.5, lat: 27.0, name: '霍尔木兹岸防阵地' },
{ lng: 50.838, lat: 28.968, name: '布什尔雷达站' },
{ lng: 51.667, lat: 32.654, name: '伊斯法罕核设施' },
],
},
{
sourceId: 'ford',
targets: [
{ lng: 51.42, lat: 35.69, name: '哈梅内伊官邸' },
{ lng: 51.41, lat: 35.72, name: '总统府/情报部' },
{ lng: 51.15, lat: 35.69, name: '梅赫拉巴德机场' },
{ lng: 46.29, lat: 38.08, name: '大不里士空军基地' },
{ lng: 47.076, lat: 34.314, name: '克尔曼沙赫导弹掩体' },
{ lng: 46.42, lat: 33.64, name: '伊拉姆导弹阵地' },
{ lng: 48.35, lat: 33.48, name: '霍拉马巴德储备库' },
],
},
]
/** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */
function parabolaPath(
@@ -157,6 +185,7 @@ export function WarMap() {
const hezbollahPathsRef = useRef<[number, number][][]>([])
const hormuzPathsRef = useRef<[number, number][][]>([])
const situation = useReplaySituation()
const { isReplayMode } = usePlaybackStore()
const { usForces, iranForces, conflictEvents = [] } = situation
const usLocs = (usForces.keyLocations || []) as KeyLoc[]
@@ -196,30 +225,52 @@ export function WarMap() {
}
}, [usForces.keyLocations, iranForces.keyLocations])
// 德黑兰到 27 个被袭目标的攻击路径(静态线条)
const mapData = situation.mapData
const attackedTargets = mapData?.attackedTargets ?? []
const strikeSources =
mapData?.strikeSources?.length > 0 ? mapData.strikeSources : FALLBACK_STRIKE_SOURCES
const strikeLines =
mapData?.strikeLines?.length > 0 ? mapData.strikeLines : FALLBACK_STRIKE_LINES
const attackPaths = useMemo(
() => ATTACKED_TARGETS.map((target) => parabolaPath(TEHRAN_SOURCE, target as [number, number])),
[]
() => attackedTargets.map((target) => parabolaPath(TEHRAN_SOURCE, target as [number, number])),
[attackedTargets]
)
attackPathsRef.current = attackPaths
const lincolnPaths = useMemo(
() => LINCOLN_STRIKE_TARGETS.map((t) => parabolaPath(LINCOLN_COORDS, t)),
[]
)
const fordPaths = useMemo(
() => FORD_STRIKE_TARGETS.map((t) => parabolaPath(FORD_COORDS, t)),
[]
)
const israelPaths = useMemo(
() => ISRAEL_STRIKE_TARGETS.map((t) => parabolaPath(ISRAEL_STRIKE_SOURCE, t)),
[]
)
// 真主党 → 以色列北部三处目标(低平弧线)
const sourceCoords = useMemo(() => {
const m: Record<string, [number, number]> = {}
strikeSources.forEach((s) => { m[s.id] = [s.lng, s.lat] })
return m
}, [strikeSources])
const lincolnPaths = useMemo(() => {
const line = strikeLines.find((l) => l.sourceId === 'lincoln')
const coords = sourceCoords.lincoln
if (!coords || !line) return []
return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
}, [strikeLines, sourceCoords])
const fordPaths = useMemo(() => {
const line = strikeLines.find((l) => l.sourceId === 'ford')
const coords = sourceCoords.ford
if (!coords || !line) return []
return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
}, [strikeLines, sourceCoords])
const israelPaths = useMemo(() => {
const line = strikeLines.find((l) => l.sourceId === 'israel')
const coords = sourceCoords.israel
if (!coords || !line) return []
return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
}, [strikeLines, sourceCoords])
// 真主党 → 以色列北部三处目标(与美/以打击弧线一致:同一 parabola 高度与动画方式)
const hezbollahSource = EXTENDED_WAR_ZONES.hezbollahStrikeSource
const hezbollahPaths = useMemo(
() => EXTENDED_WAR_ZONES.activeAttacks.map((t) => parabolaPath(HEZBOLLAH_SOURCE, t.coords, 1.5)),
[]
() =>
isReplayMode
? []
: EXTENDED_WAR_ZONES.activeAttacks.map((t) => parabolaPath(hezbollahSource, t.coords, 3)),
[hezbollahSource, isReplayMode]
)
// 伊朗不同地点 → 霍尔木兹海峡多点攻击(黄色轨迹)
const hormuzTargetPoints = useMemo(
@@ -232,6 +283,7 @@ export function WarMap() {
[]
)
const hormuzPaths = useMemo(() => {
if (isReplayMode) return []
// 使用更远的伊朗腹地/纵深位置,弧线更明显
const sources: [number, number][] = [
TEHRAN_SOURCE, // 德黑兰
@@ -241,7 +293,7 @@ export function WarMap() {
return hormuzTargetPoints.map((target, idx) =>
parabolaPath(sources[idx % sources.length], target, 3)
)
}, [hormuzTargetPoints])
}, [hormuzTargetPoints, isReplayMode])
lincolnPathsRef.current = lincolnPaths
fordPathsRef.current = fordPaths
israelPathsRef.current = israelPaths
@@ -319,28 +371,34 @@ export function WarMap() {
// 真主党当前攻击目标点
const hezbollahTargetsGeoJson = useMemo(
() => ({
type: 'FeatureCollection' as const,
features: EXTENDED_WAR_ZONES.activeAttacks.map((t) => ({
type: 'Feature' as const,
properties: { name: t.name, type: t.type, damage: t.damage },
geometry: { type: 'Point' as const, coordinates: t.coords },
})),
}),
[]
() =>
isReplayMode
? { type: 'FeatureCollection' as const, features: [] }
: {
type: 'FeatureCollection' as const,
features: EXTENDED_WAR_ZONES.activeAttacks.map((t) => ({
type: 'Feature' as const,
properties: { name: t.name, type: t.type, damage: t.damage },
geometry: { type: 'Point' as const, coordinates: t.coords },
})),
},
[isReplayMode]
)
// 霍尔木兹海峡被持续打击的海面目标(用于脉冲与标记)
const hormuzTargetsGeoJson = useMemo(
() => ({
type: 'FeatureCollection' as const,
features: hormuzTargetPoints.map((coords, idx) => ({
type: 'Feature' as const,
properties: { id: `H${idx + 1}` },
geometry: { type: 'Point' as const, coordinates: coords },
})),
}),
[hormuzTargetPoints]
() =>
isReplayMode
? { type: 'FeatureCollection' as const, features: [] }
: {
type: 'FeatureCollection' as const,
features: hormuzTargetPoints.map((coords, idx) => ({
type: 'Feature' as const,
properties: { id: `H${idx + 1}` },
geometry: { type: 'Point' as const, coordinates: coords },
})),
},
[hormuzTargetPoints, isReplayMode]
)
// 霍尔木兹海峡交战区 & 真主党势力范围(静态面)
@@ -491,7 +549,7 @@ export function WarMap() {
)
israelSrc.setData({ type: 'FeatureCollection', features })
}
// 真主党打击以色列北部:橙红光点,低平飞行
// 真主党打击以色列北部:橙红光点,与林肯/福特/以色列同一动画方式
const hezSrc = map.getSource('hezbollah-strike-dots') as
| { setData: (d: GeoJSON.FeatureCollection) => void }
| undefined
@@ -550,12 +608,12 @@ export function WarMap() {
map.setPaintProperty('gdelt-events-red-pulse', 'circle-radius', r)
map.setPaintProperty('gdelt-events-red-pulse', 'circle-opacity', opacity)
}
// 真主党攻击目标:橙红脉冲,效果与 allied-strike-targets 保持一致
// 真主党攻击目标:橙红脉冲,与 allied-strike-targets 同一周期与半径
if (map.getLayer('hezbollah-attack-targets-pulse')) {
const cycle = 2000
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
const r = Math.max(0, 30 * phase * zoomScale)
const opacity = Math.min(1, Math.max(0, 1 - phase * 1.15))
const r = Math.max(0, 35 * phase * zoomScale)
const opacity = Math.min(1, Math.max(0, 1 - phase * 1.2))
map.setPaintProperty('hezbollah-attack-targets-pulse', 'circle-radius', r)
map.setPaintProperty('hezbollah-attack-targets-pulse', 'circle-opacity', opacity)
}
@@ -891,18 +949,18 @@ export function WarMap() {
/>
</Source>
{/* 真主党对以色列北部的攻击矢量线(低平红线 */}
{/* 真主党对以色列北部的攻击矢量线(与林肯/福特/以色列线宽一致 */}
<Source id="hezbollah-attack-lines" type="geojson" data={hezbollahLinesGeoJson}>
<Layer
id="hezbollah-attack-lines"
type="line"
paint={{
'line-color': 'rgba(248, 113, 113, 0.7)',
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.6, 8, 1.2, 12, 2],
'line-color': 'rgba(248, 113, 113, 0.45)',
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
}}
/>
</Source>
{/* 真主党打击光点(沿矢量路径移动 */}
{/* 真主党打击光点(与林肯/福特/以色列光点半径与动画一致 */}
<Source
id="hezbollah-strike-dots"
type="geojson"
@@ -919,17 +977,17 @@ export function WarMap() {
id="hezbollah-strike-dots-glow"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2.5, 8, 4.5, 12, 7],
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
'circle-color': 'rgba(248, 113, 113, 0.6)',
'circle-blur': 0.25,
'circle-blur': 0.3,
}}
/>
<Layer
id="hezbollah-strike-dots-core"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 3.5],
'circle-color': '#fb923c',
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
'circle-color': '#F97316',
'circle-stroke-width': 0.5,
'circle-stroke-color': '#fff',
}}
@@ -1093,10 +1151,10 @@ export function WarMap() {
type="geojson"
data={{
type: 'FeatureCollection',
features: ALLIED_STRIKE_LOCATIONS.map((s) => ({
features: (situation.iranForces?.keyLocations ?? []).map((s) => ({
type: 'Feature' as const,
properties: { name: s.name },
geometry: { type: 'Point' as const, coordinates: s.coords },
geometry: { type: 'Point' as const, coordinates: [s.lng, s.lat] },
})),
}}
>