fix:优化整个大屏界面
This commit is contained in:
BIN
server/data.db-shm
Normal file
BIN
server/data.db-shm
Normal file
Binary file not shown.
0
server/data.db-wal
Normal file
0
server/data.db-wal
Normal file
@@ -32,7 +32,7 @@ export function BaseStatusPanel({ keyLocations, className = '' }: BaseStatusPane
|
|||||||
className={`rounded-lg border border-military-border bg-military-panel/80 p-3 font-orbitron ${className}`}
|
className={`rounded-lg border border-military-border bg-military-panel/80 p-3 font-orbitron ${className}`}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-military-text-secondary">
|
<div className="mb-2 flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-military-text-secondary">
|
||||||
<MapPin className="h-3 w-3 shrink-0" />
|
<MapPin className="h-3 w-3 shrink-0 text-blue-400" />
|
||||||
美军基地态势
|
美军基地态势
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5 text-xs tabular-nums">
|
<div className="flex flex-col gap-1.5 text-xs tabular-nums">
|
||||||
@@ -55,11 +55,17 @@ export function BaseStatusPanel({ keyLocations, className = '' }: BaseStatusPane
|
|||||||
<strong className="text-amber-500">{stats.severe}</strong>
|
<strong className="text-amber-500">{stats.severe}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-military-text-secondary">中度损毁</span>
|
<span className="flex items-center gap-1 text-military-text-secondary">
|
||||||
|
<AlertTriangle className="h-3 w-3 text-amber-400" />
|
||||||
|
中度损毁
|
||||||
|
</span>
|
||||||
<strong className="text-amber-400">{stats.moderate}</strong>
|
<strong className="text-amber-400">{stats.moderate}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-military-text-secondary">轻度损毁</span>
|
<span className="flex items-center gap-1 text-military-text-secondary">
|
||||||
|
<AlertTriangle className="h-3 w-3 text-amber-300" />
|
||||||
|
轻度损毁
|
||||||
|
</span>
|
||||||
<strong className="text-amber-300">{stats.light}</strong>
|
<strong className="text-amber-300">{stats.light}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ interface CombatLossesPanelProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOSS_ITEMS: { key: keyof Omit<CombatLosses, 'bases' | 'personnelCasualties'>; label: string; icon: typeof Plane }[] = [
|
const LOSS_ITEMS: {
|
||||||
{ key: 'aircraft', label: '战机', icon: Plane },
|
key: keyof Omit<CombatLosses, 'bases' | 'personnelCasualties'>
|
||||||
{ key: 'warships', label: '战舰', icon: Ship },
|
label: string
|
||||||
{ key: 'armor', label: '装甲', icon: Shield },
|
icon: typeof Plane
|
||||||
{ key: 'vehicles', label: '车辆', icon: Car },
|
iconColor: string
|
||||||
|
}[] = [
|
||||||
|
{ key: 'aircraft', label: '战机', icon: Plane, iconColor: 'text-sky-400' },
|
||||||
|
{ key: 'warships', label: '战舰', icon: Ship, iconColor: 'text-blue-500' },
|
||||||
|
{ key: 'armor', label: '装甲', icon: Shield, iconColor: 'text-emerald-500' },
|
||||||
|
{ key: 'vehicles', label: '车辆', icon: Car, iconColor: 'text-slate-400' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function CombatLossesPanel({ usLosses, iranLosses, className = '' }: CombatLossesPanelProps) {
|
export function CombatLossesPanel({ usLosses, iranLosses, className = '' }: CombatLossesPanelProps) {
|
||||||
@@ -34,14 +39,14 @@ export function CombatLossesPanel({ usLosses, iranLosses, className = '' }: Comb
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center gap-1 text-[10px] uppercase tracking-wider text-military-text-secondary">
|
<div className="mb-2 flex items-center gap-1 text-[10px] uppercase tracking-wider text-military-text-secondary">
|
||||||
<TrendingDown className="h-2.5 w-2.5 shrink-0" />
|
<TrendingDown className="h-2.5 w-2.5 shrink-0 text-amber-400" />
|
||||||
战损数据
|
战损数据
|
||||||
</div>
|
</div>
|
||||||
<div className="grid min-w-0 grid-cols-2 gap-x-4 gap-y-3 text-xs sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 [&>*]:min-w-0">
|
<div className="flex min-w-0 flex-wrap gap-x-4 gap-y-3 overflow-x-auto text-xs">
|
||||||
{/* 基地 - 横向第一列 */}
|
{/* 基地 */}
|
||||||
<div className="flex min-w-0 flex-col gap-0.5 overflow-hidden">
|
<div className="flex shrink-0 min-w-0 flex-col gap-0.5 overflow-visible">
|
||||||
<span className="flex shrink-0 items-center gap-1 text-military-text-secondary">
|
<span className="flex shrink-0 items-center gap-1 text-military-text-secondary">
|
||||||
<Building2 className="h-3 w-3 shrink-0" />
|
<Building2 className="h-3 w-3 shrink-0 text-amber-500" />
|
||||||
基地
|
基地
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col gap-0.5 tabular-nums">
|
<div className="flex flex-col gap-0.5 tabular-nums">
|
||||||
@@ -63,44 +68,44 @@ export function CombatLossesPanel({ usLosses, iranLosses, className = '' }: Comb
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 人员伤亡 */}
|
{/* 人员伤亡 */}
|
||||||
<div className="flex min-w-0 flex-col gap-0.5 overflow-hidden">
|
<div className="flex shrink-0 min-w-0 flex-col gap-0.5 overflow-visible">
|
||||||
<span className="flex shrink-0 items-center gap-1 text-military-text-secondary">
|
<span className="flex shrink-0 items-center gap-1 text-military-text-secondary">
|
||||||
<Users className="h-3 w-3 shrink-0" />
|
<Users className="h-3 w-3 shrink-0 text-slate-400" />
|
||||||
人员伤亡
|
人员伤亡
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col gap-0.5 tabular-nums">
|
<div className="flex flex-col gap-0.5 tabular-nums">
|
||||||
<div className="flex min-w-0 items-center gap-1 overflow-hidden" title={`美 阵亡${formatMillions(usLosses.personnelCasualties.killed)} 受伤${formatMillions(usLosses.personnelCasualties.wounded)}`}>
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-1" title={`美 阵亡${formatMillions(usLosses.personnelCasualties.killed)} 受伤${formatMillions(usLosses.personnelCasualties.wounded)}`}>
|
||||||
<span className="shrink-0 text-military-us">美</span>
|
<span className="shrink-0 text-military-us">美</span>
|
||||||
<Skull className="h-2.5 w-2.5 shrink-0 text-red-400" />
|
<Skull className="h-2.5 w-2.5 shrink-0 text-red-500" />
|
||||||
<strong className="truncate text-red-400">{formatMillions(usLosses.personnelCasualties.killed)}</strong>
|
<strong className="text-red-500">{formatMillions(usLosses.personnelCasualties.killed)}</strong>
|
||||||
<Bandage className="h-2.5 w-2.5 shrink-0 text-amber-400" />
|
<Bandage className="h-2.5 w-2.5 shrink-0 text-amber-500" />
|
||||||
<strong className="truncate text-amber-400">{formatMillions(usLosses.personnelCasualties.wounded)}</strong>
|
<strong className="text-amber-500">{formatMillions(usLosses.personnelCasualties.wounded)}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-center gap-1 overflow-hidden" title={`伊 阵亡${formatMillions(iranLosses.personnelCasualties.killed)} 受伤${formatMillions(iranLosses.personnelCasualties.wounded)}`}>
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-1" title={`伊 阵亡${formatMillions(iranLosses.personnelCasualties.killed)} 受伤${formatMillions(iranLosses.personnelCasualties.wounded)}`}>
|
||||||
<span className="shrink-0 text-military-iran">伊</span>
|
<span className="shrink-0 text-military-iran">伊</span>
|
||||||
<Skull className="h-2.5 w-2.5 shrink-0 text-red-400" />
|
<Skull className="h-2.5 w-2.5 shrink-0 text-red-500" />
|
||||||
<strong className="truncate text-red-400">{formatMillions(iranLosses.personnelCasualties.killed)}</strong>
|
<strong className="text-red-500">{formatMillions(iranLosses.personnelCasualties.killed)}</strong>
|
||||||
<Bandage className="h-2.5 w-2.5 shrink-0 text-amber-400" />
|
<Bandage className="h-2.5 w-2.5 shrink-0 text-amber-500" />
|
||||||
<strong className="truncate text-amber-400">{formatMillions(iranLosses.personnelCasualties.wounded)}</strong>
|
<strong className="text-amber-500">{formatMillions(iranLosses.personnelCasualties.wounded)}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 战机 / 战舰 / 装甲 / 车辆 */}
|
{/* 战机 / 战舰 / 装甲 / 车辆 */}
|
||||||
{LOSS_ITEMS.map(({ key, label, icon: Icon }) => (
|
{LOSS_ITEMS.map(({ key, label, icon: Icon, iconColor }) => (
|
||||||
<div key={key} className="flex min-w-0 flex-col gap-0.5 overflow-hidden">
|
<div key={key} className="flex shrink-0 min-w-0 flex-col gap-0.5 overflow-visible">
|
||||||
<span className="flex shrink-0 items-center gap-1 text-military-text-secondary">
|
<span className="flex shrink-0 items-center gap-1 text-military-text-secondary">
|
||||||
<Icon className="h-3 w-3 shrink-0" />
|
<Icon className={`h-3 w-3 shrink-0 ${iconColor}`} />
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col gap-0.5 tabular-nums">
|
<div className="flex flex-col gap-0.5 tabular-nums">
|
||||||
<div className="flex min-w-0 items-baseline gap-1 overflow-hidden">
|
<div className="flex min-w-0 items-baseline gap-1">
|
||||||
<span className="shrink-0 text-military-us">美</span>
|
<span className="shrink-0 text-military-us">美</span>
|
||||||
<strong className="truncate">{usLosses[key]}</strong>
|
<strong>{usLosses[key]}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-baseline gap-1 overflow-hidden">
|
<div className="flex min-w-0 items-baseline gap-1">
|
||||||
<span className="shrink-0 text-military-iran">伊</span>
|
<span className="shrink-0 text-military-iran">伊</span>
|
||||||
<strong className="truncate">{iranLosses[key]}</strong>
|
<strong>{iranLosses[key]}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ import type { ReactNode } from 'react'
|
|||||||
import type { ForceSummary, PowerIndex, ForceAsset } from '@/data/mockData'
|
import type { ForceSummary, PowerIndex, ForceAsset } from '@/data/mockData'
|
||||||
|
|
||||||
const ASSET_TYPE_ICONS: Record<string, ReactNode> = {
|
const ASSET_TYPE_ICONS: Record<string, ReactNode> = {
|
||||||
航母: <Ship className="h-2.5 w-2.5 shrink-0" />,
|
航母: <Ship className="h-2.5 w-2.5 shrink-0 text-blue-500" />,
|
||||||
驱逐舰: <Ship className="h-2.5 w-2.5 shrink-0" />,
|
驱逐舰: <Ship className="h-2.5 w-2.5 shrink-0 text-blue-500" />,
|
||||||
护卫舰: <Ship className="h-2.5 w-2.5 shrink-0" />,
|
护卫舰: <Ship className="h-2.5 w-2.5 shrink-0 text-blue-400" />,
|
||||||
水面舰艇: <Ship className="h-2.5 w-2.5 shrink-0" />,
|
水面舰艇: <Ship className="h-2.5 w-2.5 shrink-0 text-blue-400" />,
|
||||||
海军: <Ship className="h-2.5 w-2.5 shrink-0" />,
|
海军: <Ship className="h-2.5 w-2.5 shrink-0 text-blue-500" />,
|
||||||
战机: <Plane className="h-2.5 w-2.5 shrink-0" />,
|
战机: <Plane className="h-2.5 w-2.5 shrink-0 text-sky-400" />,
|
||||||
轰炸机: <Plane className="h-2.5 w-2.5 shrink-0" />,
|
轰炸机: <Plane className="h-2.5 w-2.5 shrink-0 text-sky-500" />,
|
||||||
防空: <Shield className="h-2.5 w-2.5 shrink-0" />,
|
防空: <Shield className="h-2.5 w-2.5 shrink-0 text-emerald-500" />,
|
||||||
导弹: <Rocket className="h-2.5 w-2.5 shrink-0" />,
|
导弹: <Rocket className="h-2.5 w-2.5 shrink-0 text-amber-500" />,
|
||||||
无人机: <Plus className="h-2.5 w-2.5 shrink-0" />,
|
无人机: <Plus className="h-2.5 w-2.5 shrink-0 text-violet-400" />,
|
||||||
准军事: <Crosshair className="h-2.5 w-2.5 shrink-0" />,
|
准军事: <Crosshair className="h-2.5 w-2.5 shrink-0 text-slate-400" />,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ForcePanelProps {
|
interface ForcePanelProps {
|
||||||
@@ -84,7 +84,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
label="资产"
|
label="资产"
|
||||||
value={formatMillions(summary.totalAssets)}
|
value={formatMillions(summary.totalAssets)}
|
||||||
variant={statVariant}
|
variant={statVariant}
|
||||||
icon={<Target className="h-2.5 w-2.5 shrink-0" />}
|
icon={<Target className="h-2.5 w-2.5 shrink-0 text-sky-400" />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
valueSize="base"
|
valueSize="base"
|
||||||
/>
|
/>
|
||||||
@@ -92,7 +92,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
label="人员"
|
label="人员"
|
||||||
value={formatMillions(summary.personnel)}
|
value={formatMillions(summary.personnel)}
|
||||||
variant={statVariant}
|
variant={statVariant}
|
||||||
icon={<Users className="h-2.5 w-2.5 shrink-0" />}
|
icon={<Users className="h-2.5 w-2.5 shrink-0 text-slate-400" />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
valueSize="base"
|
valueSize="base"
|
||||||
/>
|
/>
|
||||||
@@ -100,7 +100,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
label="海军"
|
label="海军"
|
||||||
value={formatTenThousands(summary.navalShips)}
|
value={formatTenThousands(summary.navalShips)}
|
||||||
variant={statVariant}
|
variant={statVariant}
|
||||||
icon={<Ship className="h-2.5 w-2.5 shrink-0" />}
|
icon={<Ship className="h-2.5 w-2.5 shrink-0 text-blue-500" />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
valueSize="lg"
|
valueSize="lg"
|
||||||
/>
|
/>
|
||||||
@@ -108,7 +108,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
label="航空"
|
label="航空"
|
||||||
value={formatTenThousands(summary.aircraft)}
|
value={formatTenThousands(summary.aircraft)}
|
||||||
variant={statVariant}
|
variant={statVariant}
|
||||||
icon={<Plane className="h-2.5 w-2.5 shrink-0" />}
|
icon={<Plane className="h-2.5 w-2.5 shrink-0 text-sky-400" />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
valueSize="lg"
|
valueSize="lg"
|
||||||
/>
|
/>
|
||||||
@@ -116,7 +116,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
label="无人机"
|
label="无人机"
|
||||||
value={formatTenThousands(summary.uav)}
|
value={formatTenThousands(summary.uav)}
|
||||||
variant={statVariant}
|
variant={statVariant}
|
||||||
icon={<Plus className="h-2.5 w-2.5 shrink-0" />}
|
icon={<Plus className="h-2.5 w-2.5 shrink-0 text-violet-400" />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
valueSize="lg"
|
valueSize="lg"
|
||||||
/>
|
/>
|
||||||
@@ -124,7 +124,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
label="导弹消耗"
|
label="导弹消耗"
|
||||||
value={formatMillions(summary.missileConsumed)}
|
value={formatMillions(summary.missileConsumed)}
|
||||||
variant={statVariant}
|
variant={statVariant}
|
||||||
icon={<Rocket className="h-2.5 w-2.5 shrink-0" />}
|
icon={<Rocket className="h-2.5 w-2.5 shrink-0 text-amber-500" />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
valueSize="base"
|
valueSize="base"
|
||||||
/>
|
/>
|
||||||
@@ -132,7 +132,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
label="导弹库存"
|
label="导弹库存"
|
||||||
value={formatMillions(summary.missileStock)}
|
value={formatMillions(summary.missileStock)}
|
||||||
variant={statVariant}
|
variant={statVariant}
|
||||||
icon={<Rocket className="h-2.5 w-2.5 shrink-0" />}
|
icon={<Rocket className="h-2.5 w-2.5 shrink-0 text-amber-500" />}
|
||||||
className="min-w-0"
|
className="min-w-0"
|
||||||
valueSize="base"
|
valueSize="base"
|
||||||
/>
|
/>
|
||||||
@@ -141,7 +141,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
<div className="shrink-0 border-t border-military-border px-3 py-2">
|
<div className="shrink-0 border-t border-military-border px-3 py-2">
|
||||||
<div className="mb-2 flex items-baseline justify-between">
|
<div className="mb-2 flex items-baseline justify-between">
|
||||||
<span className="flex items-center gap-1 text-[10px] uppercase tracking-wider text-military-neutral">
|
<span className="flex items-center gap-1 text-[10px] uppercase tracking-wider text-military-neutral">
|
||||||
<Gauge className="h-2.5 w-2.5 shrink-0" />
|
<Gauge className="h-2.5 w-2.5 shrink-0 text-emerald-500" />
|
||||||
综合国力
|
综合国力
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-baseline gap-1.5">
|
<div className="flex items-baseline gap-1.5">
|
||||||
@@ -161,7 +161,11 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
{ key: 'influence', label: '影响力', value: powerIndex.geopoliticalInfluence, icon: Globe },
|
{ key: 'influence', label: '影响力', value: powerIndex.geopoliticalInfluence, icon: Globe },
|
||||||
].map(({ key, label, value, icon: Icon }) => (
|
].map(({ key, label, value, icon: Icon }) => (
|
||||||
<div key={key} className="flex items-center gap-2">
|
<div key={key} className="flex items-center gap-2">
|
||||||
<Icon className="h-3 w-3 shrink-0 text-military-neutral" />
|
<Icon
|
||||||
|
className={`h-3 w-3 shrink-0 ${
|
||||||
|
key === 'military' ? 'text-emerald-500' : key === 'economic' ? 'text-blue-400' : 'text-amber-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="mb-0.5 flex justify-between text-[10px]">
|
<div className="mb-0.5 flex justify-between text-[10px]">
|
||||||
<span className="text-military-neutral">{label}</span>
|
<span className="text-military-neutral">{label}</span>
|
||||||
@@ -181,7 +185,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
|
|||||||
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden border-t border-military-border">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden border-t border-military-border">
|
||||||
<div className="shrink-0 flex items-center gap-1 border-b border-military-border bg-military-panel px-3 py-1.5 text-[10px] uppercase tracking-wider text-military-neutral">
|
<div className="shrink-0 flex items-center gap-1 border-b border-military-border bg-military-panel px-3 py-1.5 text-[10px] uppercase tracking-wider text-military-neutral">
|
||||||
<Target className="h-2.5 w-2.5 shrink-0" />
|
<Target className="h-2.5 w-2.5 shrink-0 text-sky-400" />
|
||||||
主要资产
|
主要资产
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 flex-1 overflow-hidden px-3 py-1.5">
|
<div className="min-h-0 flex-1 overflow-hidden px-3 py-1.5">
|
||||||
|
|||||||
@@ -4,15 +4,40 @@ import type { MapRef } from 'react-map-gl'
|
|||||||
import type { Map as MapboxMap } from 'mapbox-gl'
|
import type { Map as MapboxMap } from 'mapbox-gl'
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||||
import { useSituationStore } from '@/store/situationStore'
|
import { useSituationStore } from '@/store/situationStore'
|
||||||
import { ATTACKED_TARGETS } from '@/data/mapLocations'
|
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'
|
||||||
|
|
||||||
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN || ''
|
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN || ''
|
||||||
|
|
||||||
const DEFAULT_VIEW = { longitude: 52.5, latitude: 26.5, zoom: 5.2 }
|
// 相关区域 bbox:伊朗、以色列、胡塞区 (minLng, minLat, maxLng, maxLat),覆盖红蓝区域
|
||||||
|
const THEATER_BBOX = [22, 11, 64, 41] as const
|
||||||
|
const THEATER_CENTER = {
|
||||||
|
longitude: (THEATER_BBOX[0] + THEATER_BBOX[2]) / 2,
|
||||||
|
latitude: (THEATER_BBOX[1] + THEATER_BBOX[3]) / 2,
|
||||||
|
}
|
||||||
|
const DEFAULT_VIEW = { ...THEATER_CENTER, zoom: 4.2 }
|
||||||
|
|
||||||
const COUNTRIES_GEOJSON =
|
const COUNTRIES_GEOJSON =
|
||||||
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson'
|
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson'
|
||||||
|
|
||||||
|
// 胡塞武装失控区 [lng, lat] 闭环
|
||||||
|
const HOUTHI_POLYGON: [number, number][] = [
|
||||||
|
[42.7, 15.8],
|
||||||
|
[43.3, 16.5],
|
||||||
|
[45.1, 17.2],
|
||||||
|
[45.8, 15.1],
|
||||||
|
[44.2, 13.5],
|
||||||
|
[42.7, 15.8],
|
||||||
|
]
|
||||||
|
|
||||||
const IRAN_ADMIN = 'Iran'
|
const IRAN_ADMIN = 'Iran'
|
||||||
const ALLIES_ADMIN = [
|
const ALLIES_ADMIN = [
|
||||||
'Qatar',
|
'Qatar',
|
||||||
@@ -33,14 +58,39 @@ const ALLIES_ADMIN = [
|
|||||||
// 伊朗攻击源 德黑兰 [lng, lat]
|
// 伊朗攻击源 德黑兰 [lng, lat]
|
||||||
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
|
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
|
||||||
|
|
||||||
|
/** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */
|
||||||
function parabolaPath(
|
function parabolaPath(
|
||||||
start: [number, number],
|
start: [number, number],
|
||||||
end: [number, number],
|
end: [number, number],
|
||||||
height = 2
|
height = 3
|
||||||
): [number, number][] {
|
): [number, number][] {
|
||||||
const midLng = (start[0] + end[0]) / 2
|
const ctrl: [number, number] = [
|
||||||
const midLat = (start[1] + end[1]) / 2 + height
|
(start[0] + end[0]) / 2,
|
||||||
return [start, [midLng, midLat], end]
|
(start[1] + end[1]) / 2 + height,
|
||||||
|
]
|
||||||
|
// 生成多段点使曲线更平滑
|
||||||
|
const pts: [number, number][] = [start]
|
||||||
|
for (let i = 1; i < 12; i++) {
|
||||||
|
const s = i / 12
|
||||||
|
const t = 1 - s
|
||||||
|
const x = t * t * start[0] + 2 * t * s * ctrl[0] + s * s * end[0]
|
||||||
|
const y = t * t * start[1] + 2 * t * s * ctrl[1] + s * s * end[1]
|
||||||
|
pts.push([x, y])
|
||||||
|
}
|
||||||
|
pts.push(end)
|
||||||
|
return pts
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 沿路径插值,t ∈ [0,1],支持多点路径 */
|
||||||
|
function interpolateOnPath(path: [number, number][], t: number): [number, number] {
|
||||||
|
if (t <= 0) return path[0]
|
||||||
|
if (t >= 1) return path[path.length - 1]
|
||||||
|
const n = path.length - 1
|
||||||
|
const seg = Math.min(Math.floor(t * n), n - 1)
|
||||||
|
const u = (t * n) - seg
|
||||||
|
const a = path[seg]
|
||||||
|
const b = path[seg + 1]
|
||||||
|
return [a[0] + u * (b[0] - a[0]), a[1] + u * (b[1] - a[1])]
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseStatus = 'operational' | 'damaged' | 'attacked'
|
type BaseStatus = 'operational' | 'damaged' | 'attacked'
|
||||||
@@ -69,10 +119,16 @@ function toFeature(loc: KeyLoc, side: 'us' | 'iran', status?: BaseStatus) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FLIGHT_DURATION_MS = 2500 // 光点飞行单程时间
|
||||||
|
|
||||||
export function WarMap() {
|
export function WarMap() {
|
||||||
const mapRef = useRef<MapRef>(null)
|
const mapRef = useRef<MapRef>(null)
|
||||||
const animRef = useRef<number>(0)
|
const animRef = useRef<number>(0)
|
||||||
const startRef = useRef<number>(0)
|
const startRef = useRef<number>(0)
|
||||||
|
const attackPathsRef = useRef<[number, number][][]>([])
|
||||||
|
const lincolnPathsRef = useRef<[number, number][][]>([])
|
||||||
|
const fordPathsRef = useRef<[number, number][][]>([])
|
||||||
|
const israelPathsRef = useRef<[number, number][][]>([])
|
||||||
const { situation } = useSituationStore()
|
const { situation } = useSituationStore()
|
||||||
const { usForces, iranForces } = situation
|
const { usForces, iranForces } = situation
|
||||||
|
|
||||||
@@ -113,20 +169,74 @@ export function WarMap() {
|
|||||||
}
|
}
|
||||||
}, [usForces.keyLocations, iranForces.keyLocations])
|
}, [usForces.keyLocations, iranForces.keyLocations])
|
||||||
|
|
||||||
// 德黑兰到 27 个被袭目标的攻击曲线
|
// 德黑兰到 27 个被袭目标的攻击路径(静态线条)
|
||||||
|
const attackPaths = useMemo(
|
||||||
|
() => ATTACKED_TARGETS.map((target) => parabolaPath(TEHRAN_SOURCE, target as [number, number])),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
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)),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
lincolnPathsRef.current = lincolnPaths
|
||||||
|
fordPathsRef.current = fordPaths
|
||||||
|
israelPathsRef.current = israelPaths
|
||||||
|
|
||||||
|
const lincolnLinesGeoJson = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: 'FeatureCollection' as const,
|
||||||
|
features: lincolnPaths.map((coords) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'LineString' as const, coordinates: coords },
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
[lincolnPaths]
|
||||||
|
)
|
||||||
|
const fordLinesGeoJson = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: 'FeatureCollection' as const,
|
||||||
|
features: fordPaths.map((coords) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'LineString' as const, coordinates: coords },
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
[fordPaths]
|
||||||
|
)
|
||||||
|
const israelLinesGeoJson = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: 'FeatureCollection' as const,
|
||||||
|
features: israelPaths.map((coords) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'LineString' as const, coordinates: coords },
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
[israelPaths]
|
||||||
|
)
|
||||||
|
|
||||||
const attackLinesGeoJson = useMemo(
|
const attackLinesGeoJson = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
type: 'FeatureCollection' as const,
|
type: 'FeatureCollection' as const,
|
||||||
features: ATTACKED_TARGETS.map((target) => ({
|
features: attackPaths.map((coords) => ({
|
||||||
type: 'Feature' as const,
|
type: 'Feature' as const,
|
||||||
properties: {},
|
properties: {},
|
||||||
geometry: {
|
geometry: { type: 'LineString' as const, coordinates: coords },
|
||||||
type: 'LineString' as const,
|
|
||||||
coordinates: parabolaPath(TEHRAN_SOURCE, target as [number, number]),
|
|
||||||
},
|
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
[]
|
[attackPaths]
|
||||||
)
|
)
|
||||||
|
|
||||||
const hideNonBelligerentLabels = (map: MapboxMap) => {
|
const hideNonBelligerentLabels = (map: MapboxMap) => {
|
||||||
@@ -147,18 +257,27 @@ export function WarMap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const initAnimation = useRef<(map: MapboxMap) => void>(null!)
|
||||||
const map = mapRef.current?.getMap()
|
initAnimation.current = (map: MapboxMap) => {
|
||||||
if (!map) return
|
|
||||||
startRef.current = performance.now()
|
startRef.current = performance.now()
|
||||||
|
|
||||||
const tick = (t: number) => {
|
const tick = (t: number) => {
|
||||||
const elapsed = t - startRef.current
|
const elapsed = t - startRef.current
|
||||||
try {
|
try {
|
||||||
if (map.getLayer('attack-lines')) {
|
// 光点从起点飞向目标的循环动画
|
||||||
const offset = (elapsed / 16) * 0.8
|
const src = map.getSource('attack-dots') as { setData: (d: GeoJSON.FeatureCollection) => void } | undefined
|
||||||
map.setPaintProperty('attack-lines', 'line-dasharray', [2, 2])
|
const paths = attackPathsRef.current
|
||||||
map.setPaintProperty('attack-lines', 'line-dash-offset', -offset)
|
if (src && paths.length > 0) {
|
||||||
|
const features: GeoJSON.Feature<GeoJSON.Point>[] = paths.map((path, i) => {
|
||||||
|
const progress = ((elapsed / FLIGHT_DURATION_MS + i / paths.length) % 1)
|
||||||
|
const coord = interpolateOnPath(path, progress)
|
||||||
|
return {
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'Point' as const, coordinates: coord },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
src.setData({ type: 'FeatureCollection', features })
|
||||||
}
|
}
|
||||||
// damaged: 橙色闪烁 opacity 0.5 ~ 1, 约 1s 周期
|
// damaged: 橙色闪烁 opacity 0.5 ~ 1, 约 1s 周期
|
||||||
if (map.getLayer('points-damaged')) {
|
if (map.getLayer('points-damaged')) {
|
||||||
@@ -174,21 +293,100 @@ export function WarMap() {
|
|||||||
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
|
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
|
||||||
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
|
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
|
||||||
}
|
}
|
||||||
|
// 林肯号打击伊朗:蓝色光点
|
||||||
|
const lincolnSrc = map.getSource('allied-strike-dots-lincoln') as
|
||||||
|
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||||||
|
| undefined
|
||||||
|
const lincolnPaths = lincolnPathsRef.current
|
||||||
|
if (lincolnSrc && lincolnPaths.length > 0) {
|
||||||
|
const features: GeoJSON.Feature<GeoJSON.Point>[] = lincolnPaths.map(
|
||||||
|
(path, i) => {
|
||||||
|
const progress =
|
||||||
|
(elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(lincolnPaths.length, 1)) % 1
|
||||||
|
const coord = interpolateOnPath(path, progress)
|
||||||
|
return {
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'Point' as const, coordinates: coord },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lincolnSrc.setData({ type: 'FeatureCollection', features })
|
||||||
|
}
|
||||||
|
// 福特号打击伊朗:青色光点
|
||||||
|
const fordSrc = map.getSource('allied-strike-dots-ford') as
|
||||||
|
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||||||
|
| undefined
|
||||||
|
const fordPaths = fordPathsRef.current
|
||||||
|
if (fordSrc && fordPaths.length > 0) {
|
||||||
|
const features: GeoJSON.Feature<GeoJSON.Point>[] = fordPaths.map(
|
||||||
|
(path, i) => {
|
||||||
|
const progress =
|
||||||
|
(elapsed / FLIGHT_DURATION_MS + 0.3 + i / Math.max(fordPaths.length, 1)) % 1
|
||||||
|
const coord = interpolateOnPath(path, progress)
|
||||||
|
return {
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'Point' as const, coordinates: coord },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fordSrc.setData({ type: 'FeatureCollection', features })
|
||||||
|
}
|
||||||
|
// 以色列打击伊朗:浅青/白色光点
|
||||||
|
const israelSrc = map.getSource('allied-strike-dots-israel') as
|
||||||
|
| { setData: (d: GeoJSON.FeatureCollection) => void }
|
||||||
|
| undefined
|
||||||
|
const israelPaths = israelPathsRef.current
|
||||||
|
if (israelSrc && israelPaths.length > 0) {
|
||||||
|
const features: GeoJSON.Feature<GeoJSON.Point>[] = israelPaths.map(
|
||||||
|
(path, i) => {
|
||||||
|
const progress =
|
||||||
|
(elapsed / FLIGHT_DURATION_MS + 0.1 + i / Math.max(israelPaths.length, 1)) % 1
|
||||||
|
const coord = interpolateOnPath(path, progress)
|
||||||
|
return {
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'Point' as const, coordinates: coord },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
israelSrc.setData({ type: 'FeatureCollection', features })
|
||||||
|
}
|
||||||
|
// 伊朗被打击目标:蓝色脉冲 (2s 周期)
|
||||||
|
if (map.getLayer('allied-strike-targets-pulse')) {
|
||||||
|
const cycle = 2000
|
||||||
|
const phase = (elapsed % cycle) / cycle
|
||||||
|
const r = 35 * phase
|
||||||
|
const opacity = Math.max(0, 1 - phase * 1.2)
|
||||||
|
map.setPaintProperty('allied-strike-targets-pulse', 'circle-radius', r)
|
||||||
|
map.setPaintProperty('allied-strike-targets-pulse', 'circle-opacity', opacity)
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
animRef.current = requestAnimationFrame(tick)
|
animRef.current = requestAnimationFrame(tick)
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
hideNonBelligerentLabels(map)
|
hideNonBelligerentLabels(map)
|
||||||
if (map.getLayer('attack-lines')) {
|
map.fitBounds(
|
||||||
|
[[THEATER_BBOX[0], THEATER_BBOX[1]], [THEATER_BBOX[2], THEATER_BBOX[3]]],
|
||||||
|
{ padding: 40, maxZoom: 5, duration: 0 }
|
||||||
|
)
|
||||||
|
const hasAnim =
|
||||||
|
(map.getSource('attack-dots') && attackPathsRef.current.length > 0) ||
|
||||||
|
(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)
|
||||||
|
if (hasAnim) {
|
||||||
animRef.current = requestAnimationFrame(tick)
|
animRef.current = requestAnimationFrame(tick)
|
||||||
} else {
|
} else {
|
||||||
animRef.current = requestAnimationFrame(start)
|
animRef.current = requestAnimationFrame(start)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (map.isStyleLoaded()) start()
|
start()
|
||||||
else map.once('load', start)
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
return () => cancelAnimationFrame(animRef.current)
|
return () => cancelAnimationFrame(animRef.current)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -215,6 +413,33 @@ export function WarMap() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
|
{/* 图例 - 随容器自适应,避免遮挡 */}
|
||||||
|
<div className="absolute bottom-2 left-2 z-10 flex flex-wrap gap-x-3 gap-y-1 rounded bg-black/70 px-2 py-1.5 text-[9px] sm:text-[10px]">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" /> 基地
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" /> 遭袭
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 海军
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" /> 伊朗
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-sm bg-red-500/40" /> 胡塞武装
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 林肯打击
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[#06B6D4]" /> 福特打击
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-[#22D3EE]" /> 以色列打击
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<Map
|
<Map
|
||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
initialViewState={DEFAULT_VIEW}
|
initialViewState={DEFAULT_VIEW}
|
||||||
@@ -224,59 +449,60 @@ export function WarMap() {
|
|||||||
dragRotate={false}
|
dragRotate={false}
|
||||||
touchRotate={false}
|
touchRotate={false}
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
onLoad={(e) => {
|
||||||
|
// 地图加载完成后启动动画;延迟确保 Source/Layer 已挂载
|
||||||
|
setTimeout(() => initAnimation.current(e.target), 150)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* 美国海军 - 蓝色 */}
|
{/* 矢量标记:zoom 拉远变小,拉近变大 */}
|
||||||
<Source id="points-us-naval" type="geojson" data={usNaval}>
|
<Source id="points-us-naval" type="geojson" data={usNaval}>
|
||||||
<Layer
|
<Layer
|
||||||
id="points-us-naval"
|
id="points-us-naval"
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 6,
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||||||
'circle-color': '#3B82F6',
|
'circle-color': '#3B82F6',
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||||||
'circle-stroke-color': '#fff',
|
'circle-stroke-color': '#fff',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 美军基地-正常 - 绿色 */}
|
|
||||||
<Source id="points-us-base-op" type="geojson" data={usBaseOp}>
|
<Source id="points-us-base-op" type="geojson" data={usBaseOp}>
|
||||||
<Layer
|
<Layer
|
||||||
id="points-us-base-op"
|
id="points-us-base-op"
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 6,
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||||||
'circle-color': '#22C55E',
|
'circle-color': '#22C55E',
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||||||
'circle-stroke-color': '#fff',
|
'circle-stroke-color': '#fff',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 美军基地-损毁 - 橙色闪烁 */}
|
|
||||||
<Source id="points-us-base-damaged" type="geojson" data={usBaseDamaged}>
|
<Source id="points-us-base-damaged" type="geojson" data={usBaseDamaged}>
|
||||||
<Layer
|
<Layer
|
||||||
id="points-damaged"
|
id="points-damaged"
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 6,
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||||||
'circle-color': '#F97316',
|
'circle-color': '#F97316',
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||||||
'circle-stroke-color': '#fff',
|
'circle-stroke-color': '#fff',
|
||||||
'circle-opacity': 1,
|
'circle-opacity': 1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 美军基地-遭袭 - 红点 + 脉冲 */}
|
|
||||||
<Source id="points-us-base-attacked" type="geojson" data={usBaseAttacked}>
|
<Source id="points-us-base-attacked" type="geojson" data={usBaseAttacked}>
|
||||||
<Layer
|
<Layer
|
||||||
id="points-attacked-dot"
|
id="points-attacked-dot"
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 6,
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||||||
'circle-color': '#EF4444',
|
'circle-color': '#EF4444',
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||||||
'circle-stroke-color': '#fff',
|
'circle-stroke-color': '#fff',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -284,14 +510,13 @@ export function WarMap() {
|
|||||||
id="points-attacked-pulse"
|
id="points-attacked-pulse"
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 6,
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 4, 8, 12, 12, 24],
|
||||||
'circle-color': '#EF4444',
|
'circle-color': '#EF4444',
|
||||||
'circle-opacity': 0,
|
'circle-opacity': 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 伊朗 - 红色 */}
|
|
||||||
<Source
|
<Source
|
||||||
id="points-iran"
|
id="points-iran"
|
||||||
type="geojson"
|
type="geojson"
|
||||||
@@ -304,54 +529,365 @@ export function WarMap() {
|
|||||||
id="points-iran"
|
id="points-iran"
|
||||||
type="circle"
|
type="circle"
|
||||||
paint={{
|
paint={{
|
||||||
'circle-radius': 6,
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
|
||||||
'circle-color': '#EF4444',
|
'circle-color': '#EF4444',
|
||||||
'circle-stroke-width': 2,
|
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
|
||||||
'circle-stroke-color': '#fff',
|
'circle-stroke-color': '#fff',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
|
{/* 美以联军打击伊朗:路径线 */}
|
||||||
|
<Source id="allied-strike-lines-lincoln" type="geojson" data={lincolnLinesGeoJson}>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-lines-lincoln"
|
||||||
|
type="line"
|
||||||
|
paint={{
|
||||||
|
'line-color': 'rgba(96, 165, 250, 0.45)',
|
||||||
|
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
<Source id="allied-strike-lines-ford" type="geojson" data={fordLinesGeoJson}>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-lines-ford"
|
||||||
|
type="line"
|
||||||
|
paint={{
|
||||||
|
'line-color': 'rgba(6, 182, 212, 0.45)',
|
||||||
|
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
<Source id="allied-strike-lines-israel" type="geojson" data={israelLinesGeoJson}>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-lines-israel"
|
||||||
|
type="line"
|
||||||
|
paint={{
|
||||||
|
'line-color': 'rgba(34, 211, 238, 0.45)',
|
||||||
|
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 林肯号打击光点 */}
|
||||||
|
<Source
|
||||||
|
id="allied-strike-dots-lincoln"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: lincolnPaths.map((path) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-dots-lincoln-glow"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||||||
|
'circle-color': 'rgba(96, 165, 250, 0.6)',
|
||||||
|
'circle-blur': 0.3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-dots-lincoln-core"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||||||
|
'circle-color': '#3B82F6',
|
||||||
|
'circle-stroke-width': 0.5,
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 福特号打击光点 */}
|
||||||
|
<Source
|
||||||
|
id="allied-strike-dots-ford"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: fordPaths.map((path) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-dots-ford-glow"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||||||
|
'circle-color': 'rgba(6, 182, 212, 0.6)',
|
||||||
|
'circle-blur': 0.3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-dots-ford-core"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||||||
|
'circle-color': '#06B6D4',
|
||||||
|
'circle-stroke-width': 0.5,
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 以色列打击光点 */}
|
||||||
|
<Source
|
||||||
|
id="allied-strike-dots-israel"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: israelPaths.map((path) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: { type: 'Point' as const, coordinates: path[0] },
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-dots-israel-glow"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||||||
|
'circle-color': 'rgba(34, 211, 238, 0.6)',
|
||||||
|
'circle-blur': 0.3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-dots-israel-core"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||||||
|
'circle-color': '#22D3EE',
|
||||||
|
'circle-stroke-width': 0.5,
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 美军打击目标点位 (蓝色) */}
|
||||||
|
<Source
|
||||||
|
id="allied-strike-targets"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: ALLIED_STRIKE_LOCATIONS.map((s) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: { name: s.name },
|
||||||
|
geometry: { type: 'Point' as const, coordinates: s.coords },
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-targets-circle"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 5, 2.5, 8, 4],
|
||||||
|
'circle-color': '#3B82F6',
|
||||||
|
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1],
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-targets-label"
|
||||||
|
type="symbol"
|
||||||
|
layout={{
|
||||||
|
'text-field': ['get', 'name'],
|
||||||
|
'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
|
||||||
|
'text-anchor': 'top',
|
||||||
|
'text-offset': [0, 0.8],
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
'text-color': '#60A5FA',
|
||||||
|
'text-halo-width': 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="allied-strike-targets-pulse"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': 0,
|
||||||
|
'circle-color': 'rgba(96, 165, 250, 0.5)',
|
||||||
|
'circle-opacity': 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
|
||||||
|
{/* 伊朗攻击路径:细线 (矢量) */}
|
||||||
<Source id="attack-lines" type="geojson" data={attackLinesGeoJson}>
|
<Source id="attack-lines" type="geojson" data={attackLinesGeoJson}>
|
||||||
<Layer
|
<Layer
|
||||||
id="attack-lines"
|
id="attack-lines"
|
||||||
type="line"
|
type="line"
|
||||||
paint={{
|
paint={{
|
||||||
'line-color': '#ff0000',
|
'line-color': 'rgba(255, 100, 100, 0.4)',
|
||||||
'line-width': 2,
|
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
|
||||||
'line-dasharray': [2, 2],
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 光点飞行动画:从德黑兰飞向各目标 */}
|
||||||
|
<Source
|
||||||
|
id="attack-dots"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: attackPaths.map((path) => ({
|
||||||
|
type: 'Feature' as const,
|
||||||
|
properties: {},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point' as const,
|
||||||
|
coordinates: path[0],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="attack-dots-glow"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
|
||||||
|
'circle-color': 'rgba(255, 100, 100, 0.6)',
|
||||||
|
'circle-blur': 0.3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="attack-dots-core"
|
||||||
|
type="circle"
|
||||||
|
paint={{
|
||||||
|
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
|
||||||
|
'circle-color': '#ff4444',
|
||||||
|
'circle-stroke-width': 0.5,
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* 中文标注 */}
|
{/* 中文标注 - 随 zoom 自适应大小 */}
|
||||||
<Source id="labels" type="geojson" data={labelsGeoJson}>
|
<Source id="labels" type="geojson" data={labelsGeoJson}>
|
||||||
<Layer
|
<Layer
|
||||||
id="labels"
|
id="labels"
|
||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
'text-field': ['get', 'name'],
|
'text-field': ['get', 'name'],
|
||||||
'text-size': 11,
|
'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
|
||||||
'text-anchor': 'top',
|
'text-anchor': 'top',
|
||||||
'text-offset': [0, 0.8],
|
'text-offset': [0, 0.8],
|
||||||
}}
|
}}
|
||||||
paint={{
|
paint={{
|
||||||
'text-color': '#E5E7EB',
|
'text-color': '#E5E7EB',
|
||||||
'text-halo-color': 'rgba(0,0,0,0.8)',
|
'text-halo-width': 0,
|
||||||
'text-halo-width': 2,
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
|
||||||
|
{/* 胡塞武装失控区 - 伊朗红 */}
|
||||||
|
<Source
|
||||||
|
id="houthi-area"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'Feature',
|
||||||
|
properties: { name: '胡塞武装' },
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [HOUTHI_POLYGON],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="houthi-fill"
|
||||||
|
type="fill"
|
||||||
|
paint={{
|
||||||
|
'fill-color': 'rgba(239, 68, 68, 0.2)',
|
||||||
|
'fill-outline-color': '#EF4444',
|
||||||
|
'fill-opacity': 0.4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 胡塞武装标注 */}
|
||||||
|
<Source
|
||||||
|
id="houthi-label"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'Feature',
|
||||||
|
properties: { name: '胡塞武装' },
|
||||||
|
geometry: { type: 'Point', coordinates: [44, 15.5] },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="houthi-label-text"
|
||||||
|
type="symbol"
|
||||||
|
layout={{
|
||||||
|
'text-field': '胡塞武装',
|
||||||
|
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 9, 8, 12],
|
||||||
|
'text-anchor': 'center',
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
'text-color': '#EF4444',
|
||||||
|
'text-halo-width': 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
|
||||||
|
{/* 伊朗标注 */}
|
||||||
|
<Source
|
||||||
|
id="iran-label"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'Feature',
|
||||||
|
properties: { name: '伊朗' },
|
||||||
|
geometry: { type: 'Point', coordinates: [53.5, 32.5] },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="iran-label-text"
|
||||||
|
type="symbol"
|
||||||
|
layout={{
|
||||||
|
'text-field': '伊朗',
|
||||||
|
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
|
||||||
|
'text-anchor': 'center',
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
'text-color': '#EF4444',
|
||||||
|
'text-halo-width': 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
{/* 以色列标注 */}
|
||||||
|
<Source
|
||||||
|
id="israel-label"
|
||||||
|
type="geojson"
|
||||||
|
data={{
|
||||||
|
type: 'Feature',
|
||||||
|
properties: { name: '以色列' },
|
||||||
|
geometry: { type: 'Point', coordinates: [35, 31] },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layer
|
||||||
|
id="israel-label-text"
|
||||||
|
type="symbol"
|
||||||
|
layout={{
|
||||||
|
'text-field': '以色列',
|
||||||
|
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
|
||||||
|
'text-anchor': 'center',
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
'text-color': '#60A5FA',
|
||||||
|
'text-halo-width': 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
<Source id="countries" type="geojson" data={COUNTRIES_GEOJSON}>
|
<Source id="countries" type="geojson" data={COUNTRIES_GEOJSON}>
|
||||||
{/* 以色列 mesh 高亮 */}
|
{/* 伊朗区域填充 - 红色系 */}
|
||||||
<Layer
|
<Layer
|
||||||
id="israel-fill"
|
id="iran-fill"
|
||||||
type="fill"
|
type="fill"
|
||||||
filter={['==', ['get', 'ADMIN'], 'Israel']}
|
filter={['==', ['get', 'ADMIN'], IRAN_ADMIN]}
|
||||||
paint={{
|
paint={{
|
||||||
'fill-color': 'rgba(96, 165, 250, 0.25)',
|
'fill-color': 'rgba(239, 68, 68, 0.15)',
|
||||||
'fill-outline-color': '#60A5FA',
|
'fill-outline-color': '#EF4444',
|
||||||
|
'fill-opacity': 0.5,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Layer
|
<Layer
|
||||||
@@ -364,6 +900,17 @@ export function WarMap() {
|
|||||||
'line-opacity': 0.9,
|
'line-opacity': 0.9,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* 以色列区域填充 - 蓝色系 */}
|
||||||
|
<Layer
|
||||||
|
id="israel-fill"
|
||||||
|
type="fill"
|
||||||
|
filter={['==', ['get', 'ADMIN'], 'Israel']}
|
||||||
|
paint={{
|
||||||
|
'fill-color': 'rgba(96, 165, 250, 0.25)',
|
||||||
|
'fill-outline-color': '#60A5FA',
|
||||||
|
'fill-opacity': 0.6,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Layer
|
<Layer
|
||||||
id="allies-outline"
|
id="allies-outline"
|
||||||
type="line"
|
type="line"
|
||||||
|
|||||||
@@ -133,6 +133,54 @@ export const US_KEY_LOCATIONS: KeyLocItem[] = [
|
|||||||
/** 被袭击的 27 个基地坐标 [lng, lat],用于绘制攻击曲线 */
|
/** 被袭击的 27 个基地坐标 [lng, lat],用于绘制攻击曲线 */
|
||||||
export const ATTACKED_TARGETS: [number, number][] = ATTACKED_BASES.map((b) => [b.lng, b.lat])
|
export const ATTACKED_TARGETS: [number, number][] = ATTACKED_BASES.map((b) => [b.lng, b.lat])
|
||||||
|
|
||||||
|
/** 美以联军打击伊朗目标 (2026.03.01) - 中文标注,coords [lng, lat] */
|
||||||
|
export const ALLIED_STRIKE_LOCATIONS = [
|
||||||
|
// 1. 核心指挥与政治中枢
|
||||||
|
{ name: '哈梅内伊官邸', coords: [51.42, 35.69] as [number, number], type: 'Leadership' },
|
||||||
|
{ name: '总统府/情报部', coords: [51.41, 35.72] as [number, number], type: 'Leadership' },
|
||||||
|
{ name: '梅赫拉巴德机场', coords: [51.15, 35.69] as [number, number], type: 'Leadership' },
|
||||||
|
{ name: '库姆', coords: [50.88, 34.64] as [number, number], type: 'Leadership' },
|
||||||
|
// 2. 核设施与战略研究点
|
||||||
|
{ name: '伊斯法罕核设施', coords: [51.667, 32.654] as [number, number], type: 'Nuclear' },
|
||||||
|
{ name: '纳坦兹', coords: [51.916, 33.666] as [number, number], type: 'Nuclear' },
|
||||||
|
{ name: '布什尔雷达站', coords: [50.838, 28.968] as [number, number], type: 'Nuclear' },
|
||||||
|
// 3. 导弹与无人机基地
|
||||||
|
{ name: '卡拉季无人机厂', coords: [51.002, 35.808] as [number, number], type: 'UAV' },
|
||||||
|
{ name: '克尔曼沙赫导弹掩体', coords: [47.076, 34.314] as [number, number], type: 'Missile' },
|
||||||
|
{ name: '大不里士空军基地', coords: [46.29, 38.08] as [number, number], type: 'Missile' },
|
||||||
|
{ name: '伊拉姆导弹阵地', coords: [46.42, 33.64] as [number, number], type: 'Missile' },
|
||||||
|
{ name: '霍拉马巴德储备库', coords: [48.35, 33.48] as [number, number], type: 'Missile' },
|
||||||
|
// 4. 海军与南部封锁节点
|
||||||
|
{ name: '阿巴斯港海军司令部', coords: [56.27, 27.18] as [number, number], type: 'Naval' },
|
||||||
|
{ name: '米纳布', coords: [57.08, 27.13] as [number, number], type: 'Naval' },
|
||||||
|
{ name: '霍尔木兹岸防阵地', coords: [56.5, 27.0] as [number, number], type: 'Naval' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 盟军打击目标坐标 [lng, lat] */
|
||||||
|
export const ALLIED_STRIKE_TARGETS: [number, number][] = ALLIED_STRIKE_LOCATIONS.map((s) => s.coords)
|
||||||
|
|
||||||
|
/** 林肯号航母位置 [lng, lat] - 北阿拉伯海 */
|
||||||
|
export const LINCOLN_COORDS: [number, number] = [58.4215, 24.1568]
|
||||||
|
/** 福特号航母位置 [lng, lat] - 东地中海 */
|
||||||
|
export const FORD_COORDS: [number, number] = [24.1002, 35.7397]
|
||||||
|
/** 以色列打击源 [lng, lat] - 特拉维夫附近 */
|
||||||
|
export const ISRAEL_STRIKE_SOURCE: [number, number] = [34.78, 32.08]
|
||||||
|
|
||||||
|
/** 林肯号打击目标:南部海军/核设施 */
|
||||||
|
export const LINCOLN_STRIKE_TARGETS: [number, number][] = [
|
||||||
|
[56.27, 27.18], [57.08, 27.13], [56.5, 27.0], // 阿巴斯港、米纳布、霍尔木兹
|
||||||
|
[50.838, 28.968], [51.667, 32.654], // 布什尔、伊斯法罕
|
||||||
|
]
|
||||||
|
/** 福特号打击目标:北部/西部 */
|
||||||
|
export const FORD_STRIKE_TARGETS: [number, number][] = [
|
||||||
|
[51.42, 35.69], [51.41, 35.72], [51.15, 35.69], // 德黑兰核心
|
||||||
|
[46.29, 38.08], [47.076, 34.314], [46.42, 33.64], [48.35, 33.48], // 大不里士、克尔曼沙赫、伊拉姆、霍拉马巴德
|
||||||
|
]
|
||||||
|
/** 以色列打击目标:中部核设施/指挥 */
|
||||||
|
export const ISRAEL_STRIKE_TARGETS: [number, number][] = [
|
||||||
|
[50.88, 34.64], [51.916, 33.666], [51.002, 35.808], // 库姆、纳坦兹、卡拉季
|
||||||
|
]
|
||||||
|
|
||||||
export const IRAN_KEY_LOCATIONS: KeyLocItem[] = [
|
export const IRAN_KEY_LOCATIONS: KeyLocItem[] = [
|
||||||
{ name: '阿巴斯港', lat: 27.1832, lng: 56.2666, type: 'Port', region: '伊朗' },
|
{ name: '阿巴斯港', lat: 27.1832, lng: 56.2666, type: 'Port', region: '伊朗' },
|
||||||
{ name: '德黑兰', lat: 35.6892, lng: 51.389, type: 'Capital', region: '伊朗' },
|
{ name: '德黑兰', lat: 35.6892, lng: 51.389, type: 'Capital', region: '伊朗' },
|
||||||
|
|||||||
@@ -6,8 +6,20 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': { target: 'http://localhost:3001', changeOrigin: true },
|
'/api': {
|
||||||
'/ws': { target: 'ws://localhost:3001', ws: true },
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on('error', () => {}) // 后端未启动时静默失败
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/ws': {
|
||||||
|
target: 'ws://localhost:3001',
|
||||||
|
ws: true,
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on('error', () => {}) // 后端未启动时静默失败
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Reference in New Issue
Block a user