diff --git a/crawler/__pycache__/db_merge.cpython-39.pyc b/crawler/__pycache__/db_merge.cpython-39.pyc deleted file mode 100644 index a827d5f..0000000 Binary files a/crawler/__pycache__/db_merge.cpython-39.pyc and /dev/null differ diff --git a/crawler/__pycache__/indicator_smooth.cpython-311.pyc b/crawler/__pycache__/indicator_smooth.cpython-311.pyc index 80018f7..24c8037 100644 Binary files a/crawler/__pycache__/indicator_smooth.cpython-311.pyc and b/crawler/__pycache__/indicator_smooth.cpython-311.pyc differ 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() { > 以色列打击 + +