From 7e0c209d9addafa039932066ecec4f53d1a2a7fd Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 6 Mar 2026 17:43:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=96=B0=E5=A2=9E=E5=A4=84=20=E6=88=98?= =?UTF-8?q?=E5=BD=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawler/__pycache__/db_merge.cpython-39.pyc | Bin 12367 -> 0 bytes .../indicator_smooth.cpython-311.pyc | Bin 3675 -> 3650 bytes server/seed.js | 9 + src/components/BaseStatusPanel.tsx | 1 - src/components/IranBaseStatusPanel.tsx | 1 - src/components/WarMap.tsx | 585 +++++++++++++++++- 6 files changed, 576 insertions(+), 20 deletions(-) delete mode 100644 crawler/__pycache__/db_merge.cpython-39.pyc diff --git a/crawler/__pycache__/db_merge.cpython-39.pyc b/crawler/__pycache__/db_merge.cpython-39.pyc deleted file mode 100644 index a827d5f530410837bf027e8960366441e5dca164..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12367 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 diff --git a/crawler/__pycache__/indicator_smooth.cpython-311.pyc b/crawler/__pycache__/indicator_smooth.cpython-311.pyc index 80018f7d0c65ea427c985930fcab798cf22f13f0..24c8037882cc8e6fc4878199cb1282533175cd0b 100644 GIT binary patch delta 14 VcmcaDb4X^xH6}*s%^#SuxdAT)1(g5* delta 39 ucmX>kb6aM^H6|$&{m|mnqGJ7&#JtSZ9Q~Zkvdp}6{m@`X{ms{zvbg~+cMggG diff --git a/server/seed.js b/server/seed.js index 091cdb1..d39a082 100644 --- a/server/seed.js +++ b/server/seed.js @@ -162,6 +162,9 @@ function seed() { ['israel', '以色列', 34.78, 32.08], ['lincoln', '林肯号航母', 58.4215, 24.1568], ['ford', '福特号航母', 24.1002, 35.7397], + ['virginia_srilanka', '洛杉矶级攻击核潜艇(斯里兰卡外海)', 78.5, 5.2], + ['tomahawk_arabian', '阿拉伯海潜艇(战斧)', 59.2, 23.0], + ['arabian_sea_sub_torpedo', '阿拉伯海潜艇(鱼雷)', 59.2, 23.0], ] strikeSources.forEach((r) => insertStrikeSource.run(...r)) @@ -202,6 +205,12 @@ function seed() { israelLebanonTargets.forEach(([lng, lat, name, struckAt]) => insertStrikeLine.run('israel', lng, lat, name, struckAt)) lincolnTargets.forEach(([lng, lat, name, struckAt]) => insertStrikeLine.run('lincoln', lng, lat, name, struckAt)) fordTargets.forEach(([lng, lat, name, struckAt]) => insertStrikeLine.run('ford', lng, lat, name, struckAt)) + const virginiaSrilankaTargets = [[79.8, 6.5, '德纳号轻型护卫舰', '2026-02-28T08:00:00.000Z']] + virginiaSrilankaTargets.forEach(([lng, lat, name, struckAt]) => insertStrikeLine.run('virginia_srilanka', lng, lat, name, struckAt)) + const tomahawkArabianTargets = [[56.26, 27.18, '沙希德·鲁德基号/无人机航母', '2026-02-28T03:00:00.000Z']] + tomahawkArabianTargets.forEach(([lng, lat, name, struckAt]) => insertStrikeLine.run('tomahawk_arabian', lng, lat, name, struckAt)) + const arabianSubTorpedoTargets = [[56.26, 27.18, '沙希德·鲁德基号/无人机航母', '2026-02-28T03:00:00.000Z']] + arabianSubTorpedoTargets.forEach(([lng, lat, name, struckAt]) => insertStrikeLine.run('arabian_sea_sub_torpedo', lng, lat, name, struckAt)) try { db.exec(` diff --git a/src/components/BaseStatusPanel.tsx b/src/components/BaseStatusPanel.tsx index ef4bb87..22247a6 100644 --- a/src/components/BaseStatusPanel.tsx +++ b/src/components/BaseStatusPanel.tsx @@ -33,7 +33,6 @@ export function BaseStatusPanel({ keyLocations, className = '' }: BaseStatusPane
美军基地态势 - (随爬虫 AI 实时更新)
diff --git a/src/components/IranBaseStatusPanel.tsx b/src/components/IranBaseStatusPanel.tsx index 6661429..464f9b8 100644 --- a/src/components/IranBaseStatusPanel.tsx +++ b/src/components/IranBaseStatusPanel.tsx @@ -32,7 +32,6 @@ export function IranBaseStatusPanel({ keyLocations = [], className = '' }: IranB
伊朗基地态势 - (随爬虫 AI 实时更新)
diff --git a/src/components/WarMap.tsx b/src/components/WarMap.tsx index cd496c1..3d2db1d 100644 --- a/src/components/WarMap.tsx +++ b/src/components/WarMap.tsx @@ -60,6 +60,8 @@ const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892] /** 攻击动画时间衰减:N 天内按天衰减(脉冲缩小、频次降低),超出仅保留减弱呼吸效果;天数可在编辑面板配置 */ const MS_PER_DAY = 24 * 60 * 60 * 1000 +/** 手动添加的打击源:始终参与实时渲染列表,用 strikeCutoffDays 做衰减;仅在图例选中该项时不衰减 */ +const MANUAL_STRIKE_SOURCE_IDS = new Set(['virginia_srilanka', 'tomahawk_arabian', 'arabian_sea_sub_torpedo']) function isWithinAnimationWindow( iso: string | null | undefined, referenceTime: string, @@ -105,8 +107,11 @@ const FALLBACK_STRIKE_SOURCES: { id: string; name: string; lng: number; lat: num { 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 }, + { id: 'virginia_srilanka', name: '洛杉矶级攻击核潜艇(斯里兰卡外海)', lng: 78.5, lat: 5.2 }, + { id: 'tomahawk_arabian', name: '阿拉伯海潜艇(战斧)', lng: 59.2, lat: 23.0 }, + { id: 'arabian_sea_sub_torpedo', name: '阿拉伯海潜艇(鱼雷)', lng: 59.2, lat: 23.0 }, ] -const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: number; name?: string }[] }[] = [ +const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: number; name?: string; struck_at?: string | null }[] }[] = [ { sourceId: 'israel', targets: [ @@ -143,6 +148,24 @@ const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: nu { lng: 48.35, lat: 33.48, name: '霍拉马巴德储备库' }, ], }, + { + sourceId: 'virginia_srilanka', + targets: [ + { lng: 79.8, lat: 6.5, name: '德纳号轻型护卫舰', struck_at: '2026-02-28T08:00:00.000Z' }, + ], + }, + { + sourceId: 'tomahawk_arabian', + targets: [ + { lng: 56.26, lat: 27.18, name: '沙希德·鲁德基号/无人机航母', struck_at: '2026-02-28T03:00:00.000Z' }, + ], + }, + { + sourceId: 'arabian_sea_sub_torpedo', + targets: [ + { lng: 56.26, lat: 27.18, name: '沙希德·鲁德基号/无人机航母', struck_at: '2026-02-28T03:00:00.000Z' }, + ], + }, ] /** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */ @@ -232,6 +255,9 @@ export function WarMap() { const lincolnPathsRef = useRef<[number, number][][]>([]) const fordPathsRef = useRef<[number, number][][]>([]) const israelPathsRef = useRef<[number, number][][]>([]) + const virginiaPathsRef = useRef<[number, number][][]>([]) + const tomahawkPathsRef = useRef<[number, number][][]>([]) + const arabianSubTorpedoPathsRef = useRef<[number, number][][]>([]) const hezbollahPathsRef = useRef<[number, number][][]>([]) const hormuzPathsRef = useRef<[number, number][][]>([]) const [legendOpen, setLegendOpen] = useState(true) @@ -240,6 +266,8 @@ export function WarMap() { | 'lincoln' | 'ford' | 'israel' + | 'virginia_srilanka' + | 'tomahawk_arabian' | 'hezbollah' | 'iranToUs' | 'iran' @@ -356,21 +384,40 @@ export function WarMap() { }, [usForces.keyLocations, iranForces.keyLocations]) const mapData = situation.mapData - const strikeSources = - mapData?.strikeSources?.length > 0 ? mapData.strikeSources : FALLBACK_STRIKE_SOURCES - const strikeLines = - mapData?.strikeLines?.length > 0 ? mapData.strikeLines : FALLBACK_STRIKE_LINES + /** 打击源:API 有则用,并合并手动场景源(斯里兰卡潜艇、阿巴斯港战斧+潜艇鱼雷),缺则从 fallback 补全 */ + const strikeSources = useMemo(() => { + const fromApi = mapData?.strikeSources?.length > 0 ? mapData.strikeSources : FALLBACK_STRIKE_SOURCES + const ids = new Set((fromApi as { id: string }[]).map((s) => s.id)) + const toAdd = FALLBACK_STRIKE_SOURCES.filter((s) => !ids.has(s.id)) + return toAdd.length > 0 ? [...fromApi, ...toAdd] : fromApi + }, [mapData?.strikeSources]) + /** 打击线:API 有则用 API,并始终合并斯里兰卡/阿巴斯港(战斧+潜艇) fallback,保证场景必有数据可渲染 */ + const strikeLines = useMemo(() => { + const fromApi = mapData?.strikeLines?.length > 0 ? mapData.strikeLines : FALLBACK_STRIKE_LINES + const hasVirginia = fromApi.some((l) => l.sourceId === 'virginia_srilanka') + const hasTomahawk = fromApi.some((l) => l.sourceId === 'tomahawk_arabian') + const hasArabianSubTorpedo = fromApi.some((l) => l.sourceId === 'arabian_sea_sub_torpedo') + const extra = [ + ...(hasVirginia ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'virginia_srilanka')), + ...(hasTomahawk ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'tomahawk_arabian')), + ...(hasArabianSubTorpedo ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'arabian_sea_sub_torpedo')), + ] + return extra.length > 0 ? [...fromApi, ...extra] : fromApi + }, [mapData?.strikeLines]) /** 伊朗→美军基地:仅用 DB 数据,N 天内显示飞行动画 */ const strikeCutoffDays = situation.animationConfig?.strikeCutoffDays ?? 5 - /** 实时模式:仅渲染最近 1 天内的进展(主视角);默认开启 */ - const REALTIME_CUTOFF_DAYS = 1 + /** 实时模式:渲染最近 5 天内的进展(主视角);全部=不限时长 */ + const REALTIME_CUTOFF_DAYS = 5 + const UNLIMITED_CUTOFF_DAYS = 365 const [isRealtimeView, setIsRealtimeView] = useState(true) const isRealtimeViewRef = useRef(true) isRealtimeViewRef.current = isRealtimeView - /** 未选中单项时:全部=用配置天数,实时=1 天;选中某项时用配置天数(该单项完整动画) */ + /** 未选中单项时:全部=不限时长(365天),实时=5 天;选中某项时用 strikeCutoffDays(该单项完整动画) */ const effectiveCutoffDays = - strikeLegendFilter === null && isRealtimeView ? REALTIME_CUTOFF_DAYS : strikeCutoffDays + strikeLegendFilter === null + ? (isRealtimeView ? REALTIME_CUTOFF_DAYS : UNLIMITED_CUTOFF_DAYS) + : strikeCutoffDays const attackPaths = useMemo(() => { const attacked = (usForces.keyLocations || []).filter( @@ -396,7 +443,7 @@ export function WarMap() { return m }, [strikeSources]) - /** 盟军打击线:仅用 effectiveCutoffDays 内目标显示飞行动画(全部=配置天数,实时=1 天) */ + /** 盟军打击线:仅用 effectiveCutoffDays 内目标显示飞行动画(全部=不限时长,实时=5 天) */ const filterTargetsByAnimationWindow = useMemo( () => (targets: { lng: number; lat: number; struck_at?: string | null }[]) => targets.filter((t) => isWithinAnimationWindow(t.struck_at ?? null, referenceTime, effectiveCutoffDays)), @@ -448,6 +495,41 @@ export function WarMap() { return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat])) }, [strikeLines, sourceCoords]) + /** 场景 A:斯里兰卡深海伏击 — 鱼雷轨迹为直线(潜艇 [78.5,5.2] → 驱逐舰 [79.8,6.5]),不采用抛物线 */ + const virginiaSrilankaPaths = useMemo(() => { + const line = strikeLines.find((l) => l.sourceId === 'virginia_srilanka') + const coords = sourceCoords.virginia_srilanka + if (!coords || !line) return [] + return line.targets.map((t) => [coords, [t.lng, t.lat]] as [number, number][]) + }, [strikeLines, sourceCoords]) + /** 场景 B:阿拉伯海战斧 → 阿巴斯港,抛物线弹道 */ + const tomahawkArabianPaths = useMemo(() => { + const line = strikeLines.find((l) => l.sourceId === 'tomahawk_arabian') + const coords = sourceCoords.tomahawk_arabian + if (!coords || !line) return [] + return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat])) + }, [strikeLines, sourceCoords]) + /** 上述两场景在时间窗口内的路径(与林肯/福特/以色列一致,用于实时模式) */ + const effectiveVirginiaSrilankaPaths = useMemo(() => { + const line = strikeLines.find((l) => l.sourceId === 'virginia_srilanka') + const coords = sourceCoords.virginia_srilanka + if (!coords || !line) return [] + return filterTargetsByAnimationWindow(line.targets).map((t) => [coords, [t.lng, t.lat]] as [number, number][]) + }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow]) + const effectiveTomahawkArabianPaths = useMemo(() => { + const line = strikeLines.find((l) => l.sourceId === 'tomahawk_arabian') + const coords = sourceCoords.tomahawk_arabian + if (!coords || !line) return [] + return filterTargetsByAnimationWindow(line.targets).map((t) => parabolaPath(coords, [t.lng, t.lat])) + }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow]) + /** 场景 B:阿巴斯港 — 同艇潜射/鱼雷轨迹(直线,与战斧同时) */ + const arabianSubTorpedoPaths = useMemo(() => { + const line = strikeLines.find((l) => l.sourceId === 'arabian_sea_sub_torpedo') + const coords = sourceCoords.arabian_sea_sub_torpedo + if (!coords || !line) return [] + return line.targets.map((t) => [coords, [t.lng, t.lat]] as [number, number][]) + }, [strikeLines, sourceCoords]) + /** 黎巴嫩→以色列:攻击源为黎巴嫩境内多处(提尔、西顿、巴勒贝克等),目标为以色列北部 */ const hezbollahPaths = useMemo(() => { const sources = EXTENDED_WAR_ZONES.lebanonStrikeSources @@ -481,21 +563,24 @@ export function WarMap() { const warMapData = useWarMapData() - /** 当前参与动画的目标的最小衰减系数,用于脉冲范围与攻击频次(缩小脉冲、拉长飞行周期) */ + /** 当前参与动画的目标的最小衰减系数;手动打击(virginia/tomahawk)用 strikeCutoffDays 参与衰减,其余用 effectiveCutoffDays */ const animationDecayFactor = useMemo(() => { const decays: number[] = [] ;(usForces.keyLocations || []) .filter((l) => l.status === 'attacked' && l.attacked_at && isWithinAnimationWindow(l.attacked_at, referenceTime, effectiveCutoffDays)) .forEach((l) => decays.push(getDecayFactor(l.attacked_at ?? null, referenceTime, effectiveCutoffDays))) for (const line of strikeLines) { - for (const t of filterTargetsByAnimationWindow(line.targets)) { - decays.push(getDecayFactor(t.struck_at ?? null, referenceTime, effectiveCutoffDays)) + const cutoffDays = MANUAL_STRIKE_SOURCE_IDS.has(line.sourceId) ? strikeCutoffDays : effectiveCutoffDays + const inWindow = (t: { struck_at?: string | null }) => isWithinAnimationWindow(t.struck_at ?? null, referenceTime, cutoffDays) + for (const t of line.targets) { + if (!inWindow(t)) continue + decays.push(getDecayFactor((t as { struck_at?: string | null }).struck_at ?? null, referenceTime, cutoffDays)) } } if (hormuzPaths.length > 0) decays.push(1) if (hezbollahPaths.length > 0) decays.push(1) return decays.length > 0 ? Math.min(...decays) : 1 - }, [usForces.keyLocations, strikeLines, referenceTime, effectiveCutoffDays, filterTargetsByAnimationWindow, hormuzPaths.length, hezbollahPaths.length]) + }, [usForces.keyLocations, strikeLines, referenceTime, effectiveCutoffDays, strikeCutoffDays, hormuzPaths.length, hezbollahPaths.length]) const animationDecayRef = useRef(1) animationDecayRef.current = animationDecayFactor @@ -534,9 +619,31 @@ export function WarMap() { const effectiveHormuzPaths = strikeLegendFilter === 'hormuz' || strikeLegendFilter === 'iran' ? hormuzPaths : strikeLegendFilter === null ? hormuzPaths : [] + /** 斯里兰卡伏击 / 阿巴斯港突袭(手动打击):实时模式下始终进入渲染列表并做衰减;图例选中时完整动画不衰减 */ + const effectiveVirginiaSrilankaPathsDisplay = + strikeLegendFilter === 'virginia_srilanka' + ? virginiaSrilankaPaths + : strikeLegendFilter === null + ? virginiaSrilankaPaths + : [] + const effectiveTomahawkArabianPathsDisplay = + strikeLegendFilter === 'tomahawk_arabian' + ? tomahawkArabianPaths + : strikeLegendFilter === null + ? tomahawkArabianPaths + : [] + /** 阿巴斯港潜艇直线(与战斧同时):与 tomahawk_arabian 同显 */ + const effectiveArabianSubTorpedoPathsDisplay = + strikeLegendFilter === 'tomahawk_arabian' || strikeLegendFilter === null + ? arabianSubTorpedoPaths + : [] + lincolnPathsRef.current = effectiveLincolnPaths fordPathsRef.current = effectiveFordPaths israelPathsRef.current = effectiveIsraelPaths + virginiaPathsRef.current = effectiveVirginiaSrilankaPathsDisplay + tomahawkPathsRef.current = effectiveTomahawkArabianPathsDisplay + arabianSubTorpedoPathsRef.current = effectiveArabianSubTorpedoPathsDisplay hezbollahPathsRef.current = effectiveHezbollahPaths hormuzPathsRef.current = effectiveHormuzPaths attackPathsRef.current = effectiveAttackPaths @@ -566,6 +673,38 @@ export function WarMap() { if (sourceCoords.israel) push(sourceCoords.israel) flattenPaths(israelPathsAll) break + case 'virginia_srilanka': { + const src = sourceCoords.virginia_srilanka + const paths = effectiveVirginiaSrilankaPathsDisplay + if (src && paths.length > 0) { + const tgt = paths[0][paths[0].length - 1] + const padLine = 1.2 + return [ + Math.min(src[0], tgt[0]) - padLine, + Math.min(src[1], tgt[1]) - padLine, + Math.max(src[0], tgt[0]) + padLine, + Math.max(src[1], tgt[1]) + padLine, + ] + } + break + } + case 'tomahawk_arabian': { + const src = sourceCoords.tomahawk_arabian ?? sourceCoords.arabian_sea_sub_torpedo + const paths = [...effectiveTomahawkArabianPathsDisplay, ...effectiveArabianSubTorpedoPathsDisplay] + if (src && paths.length > 0) { + const allCoords = paths.flatMap((p) => p) + const lngs = allCoords.map((c) => c[0]) + const lats = allCoords.map((c) => c[1]) + const padLine = 1.2 + return [ + Math.min(...lngs) - padLine, + Math.min(...lats) - padLine, + Math.max(...lngs) + padLine, + Math.max(...lats) + padLine, + ] + } + break + } case 'hezbollah': flattenPaths(hezbollahPaths) break @@ -616,6 +755,9 @@ export function WarMap() { lincolnPathsAll, fordPathsAll, israelPathsAll, + effectiveVirginiaSrilankaPathsDisplay, + effectiveTomahawkArabianPathsDisplay, + effectiveArabianSubTorpedoPathsDisplay, hezbollahPaths, attackPathsAll, ] @@ -660,16 +802,113 @@ export function WarMap() { }), [effectiveIsraelPaths] ) - /** 盟军打击目标点位(林肯/福特/以色列→伊朗+以色列→黎巴嫩)。全部/单项:显示全部目标;实时:仅 effectiveCutoffDays 内 */ + /** 场景 A:斯里兰卡鱼雷轨迹 — 直线、蓝色虚线(Mk 48 ADCAP 声纳制导路径) */ + const submarineTorpedoLinesGeoJson = useMemo( + () => ({ + type: 'FeatureCollection' as const, + features: + strikeLegendFilter === null || strikeLegendFilter === 'virginia_srilanka' + ? effectiveVirginiaSrilankaPathsDisplay.map((coords) => ({ + type: 'Feature' as const, + properties: { weapon: 'Mk 48 Torpedo', status: 'Impact Confirmed' }, + geometry: { type: 'LineString' as const, coordinates: coords }, + })) + : [], + }), + [effectiveVirginiaSrilankaPathsDisplay, strikeLegendFilter] + ) + /** 场景 B:阿拉伯海战斧弹道 — 抛物线(阿巴斯港/沙希德·鲁德基号) */ + const tomahawkStrikeLinesGeoJson = useMemo( + () => ({ + type: 'FeatureCollection' as const, + features: + strikeLegendFilter === null || strikeLegendFilter === 'tomahawk_arabian' + ? effectiveTomahawkArabianPathsDisplay.map((coords) => ({ + type: 'Feature' as const, + properties: { target: 'Drone Carrier Shahid Roudaki', damage: 'Critical/Sunk' }, + geometry: { type: 'LineString' as const, coordinates: coords }, + })) + : [], + }), + [effectiveTomahawkArabianPathsDisplay, strikeLegendFilter] + ) + /** 场景 B:阿巴斯港同艇潜射/鱼雷 — 直线(与战斧同时) */ + const arabianSubTorpedoLinesGeoJson = useMemo( + () => ({ + type: 'FeatureCollection' as const, + features: + strikeLegendFilter === null || strikeLegendFilter === 'tomahawk_arabian' + ? effectiveArabianSubTorpedoPathsDisplay.map((coords) => ({ + type: 'Feature' as const, + properties: { weapon: 'Sub torpedo', status: 'Impact Confirmed' }, + geometry: { type: 'LineString' as const, coordinates: coords }, + })) + : [], + }), + [effectiveArabianSubTorpedoPathsDisplay, strikeLegendFilter] + ) + /** 战场态势标注:红圈×斯里兰卡外海驱逐舰沉没;紫色◆阿巴斯港高价值目标摧毁 */ + const tacticalSymbolsGeoJson = useMemo(() => { + const showAll = strikeLegendFilter === null + const showVirginia = strikeLegendFilter === 'virginia_srilanka' + const showTomahawk = strikeLegendFilter === 'tomahawk_arabian' + const features: GeoJSON.Feature[] = [] + if (showAll || showVirginia) { + features.push({ + type: 'Feature' as const, + properties: { symbol: 'destruction', name: 'Destroyed Surface Combatant' }, + geometry: { type: 'Point' as const, coordinates: [79.8, 6.5] as [number, number] }, + }) + } + if (showAll || showTomahawk) { + features.push({ + type: 'Feature' as const, + properties: { + symbol: 'explosion', + name: 'Port Facility/High Value Asset Neutralized', + target: 'Drone Carrier Shahid Roudaki', + damage: 'Critical/Sunk', + }, + geometry: { type: 'Point' as const, coordinates: [56.26, 27.18] as [number, number] }, + }) + } + return { type: 'FeatureCollection' as const, features } + }, [strikeLegendFilter]) + /** 盟军打击发起点地标(林肯/福特/以色列/斯里兰卡潜艇/阿拉伯海潜艇):样式统一,仅阵营颜色不同 */ + const alliedStrikeSourcesGeoJson = useMemo( + () => ({ + type: 'FeatureCollection' as const, + features: strikeSources.map((s) => ({ + type: 'Feature' as const, + properties: { id: s.id, name: s.name }, + geometry: { type: 'Point' as const, coordinates: [s.lng, s.lat] as [number, number] }, + })), + }), + [strikeSources] + ) + + /** 盟军打击目标点位(林肯/福特/以色列→伊朗+以色列→黎巴嫩+斯里兰卡/阿巴斯港)。全部/单项:显示全部目标;实时:仅 effectiveCutoffDays 内 */ const alliedStrikeTargetsFeatures = useMemo(() => { const out: GeoJSON.Feature[] = [] const onlyRealtimeFilter = strikeLegendFilter === null && isRealtimeView for (const line of strikeLines) { - if (strikeLegendFilter != null && strikeLegendFilter !== 'lincoln' && strikeLegendFilter !== 'ford' && strikeLegendFilter !== 'israel') continue + if ( + strikeLegendFilter != null && + strikeLegendFilter !== 'lincoln' && + strikeLegendFilter !== 'ford' && + strikeLegendFilter !== 'israel' && + strikeLegendFilter !== 'virginia_srilanka' && + strikeLegendFilter !== 'tomahawk_arabian' && + strikeLegendFilter !== 'arabian_sea_sub_torpedo' + ) + continue if (strikeLegendFilter === 'lincoln' && line.sourceId !== 'lincoln') continue if (strikeLegendFilter === 'ford' && line.sourceId !== 'ford') continue if (strikeLegendFilter === 'israel' && line.sourceId !== 'israel') continue + if (strikeLegendFilter === 'virginia_srilanka' && line.sourceId !== 'virginia_srilanka') continue + if (strikeLegendFilter === 'tomahawk_arabian' && line.sourceId !== 'tomahawk_arabian' && line.sourceId !== 'arabian_sea_sub_torpedo') continue + if (strikeLegendFilter === 'arabian_sea_sub_torpedo' && line.sourceId !== 'arabian_sea_sub_torpedo') continue for (const t of line.targets) { if (onlyRealtimeFilter && !isWithinAnimationWindow((t as { struck_at?: string | null }).struck_at ?? null, referenceTime, effectiveCutoffDays)) continue @@ -929,6 +1168,60 @@ export function WarMap() { }) israelSrc.setData({ type: 'FeatureCollection', features }) } + // 斯里兰卡伏击:鱼雷轨迹蓝色光点(直线路径) + const virginiaSrc = map.getSource('allied-strike-dots-virginia') as + | { setData: (d: GeoJSON.FeatureCollection) => void } + | undefined + const virginiaPaths = virginiaPathsRef.current + if (virginiaSrc && virginiaPaths.length > 0) { + const features: GeoJSON.Feature[] = [] + virginiaPaths.forEach((path, i) => { + if (i % step !== 0) return + const progress = (elapsed / FLIGHT_DURATION_MS + 0.6 + i / Math.max(virginiaPaths.length, 1)) % 1 + features.push({ + type: 'Feature' as const, + properties: {}, + geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) }, + }) + }) + virginiaSrc.setData({ type: 'FeatureCollection', features }) + } + // 阿巴斯港突袭:战斧弹道紫色光点(抛物线路径) + const tomahawkSrc = map.getSource('allied-strike-dots-tomahawk') as + | { setData: (d: GeoJSON.FeatureCollection) => void } + | undefined + const tomahawkPaths = tomahawkPathsRef.current + if (tomahawkSrc && tomahawkPaths.length > 0) { + const features: GeoJSON.Feature[] = [] + tomahawkPaths.forEach((path, i) => { + if (i % step !== 0) return + const progress = (elapsed / FLIGHT_DURATION_MS + 0.4 + i / Math.max(tomahawkPaths.length, 1)) % 1 + features.push({ + type: 'Feature' as const, + properties: {}, + geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) }, + }) + }) + tomahawkSrc.setData({ type: 'FeatureCollection', features }) + } + // 阿巴斯港同艇潜射/鱼雷光点(直线路径,与战斧同时) + const arabianSubSrc = map.getSource('allied-strike-dots-arabian-sub') as + | { setData: (d: GeoJSON.FeatureCollection) => void } + | undefined + const arabianSubPaths = arabianSubTorpedoPathsRef.current + if (arabianSubSrc && arabianSubPaths.length > 0) { + const features: GeoJSON.Feature[] = [] + arabianSubPaths.forEach((path, i) => { + if (i % step !== 0) return + const progress = (elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(arabianSubPaths.length, 1)) % 1 + features.push({ + type: 'Feature' as const, + properties: {}, + geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) }, + }) + }) + arabianSubSrc.setData({ type: 'FeatureCollection', features }) + } // 真主党打击以色列北部:橙红光点,与林肯/福特/以色列同一动画方式 const hezSrc = map.getSource('hezbollah-strike-dots') as | { setData: (d: GeoJSON.FeatureCollection) => void } @@ -1085,6 +1378,9 @@ export function WarMap() { (map.getSource('allied-strike-dots-lincoln') && lincolnPathsRef.current.length > 0) || (map.getSource('allied-strike-dots-ford') && fordPathsRef.current.length > 0) || (map.getSource('allied-strike-dots-israel') && israelPathsRef.current.length > 0) || + (map.getSource('allied-strike-dots-virginia') && virginiaPathsRef.current.length > 0) || + (map.getSource('allied-strike-dots-tomahawk') && tomahawkPathsRef.current.length > 0) || + (map.getSource('allied-strike-dots-arabian-sub') && arabianSubTorpedoPathsRef.current.length > 0) || (map.getSource('hezbollah-strike-dots') && hezbollahPathsRef.current.length > 0) || (map.getSource('iran-hormuz-dots') && hormuzPathsRef.current.length > 0) || map.getSource('kurdish-pincer-growth') || @@ -1262,7 +1558,7 @@ export function WarMap() { fitToTheater({ duration: 500 }) }} className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === null && isRealtimeView ? 'ring-1 ring-amber-400/80 bg-amber-400/10' : 'hover:bg-white/10'}`} - title="主视角,仅渲染最新进展(近 1 天)" + title="主视角,仅渲染最新进展(近 5 天)" > 实时 @@ -1290,6 +1586,22 @@ export function WarMap() { > 以色列打击 + +