diff --git a/server/data.db-shm b/server/data.db-shm new file mode 100644 index 0000000..fe9ac28 Binary files /dev/null and b/server/data.db-shm differ diff --git a/server/data.db-wal b/server/data.db-wal new file mode 100644 index 0000000..e69de29 diff --git a/src/components/BaseStatusPanel.tsx b/src/components/BaseStatusPanel.tsx index b267a96..b900b56 100644 --- a/src/components/BaseStatusPanel.tsx +++ b/src/components/BaseStatusPanel.tsx @@ -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}`} >
- + 美军基地态势
@@ -55,11 +55,17 @@ export function BaseStatusPanel({ keyLocations, className = '' }: BaseStatusPane {stats.severe}
- 中度损毁 + + + 中度损毁 + {stats.moderate}
- 轻度损毁 + + + 轻度损毁 + {stats.light}
diff --git a/src/components/CombatLossesPanel.tsx b/src/components/CombatLossesPanel.tsx index aae140c..dff2fbd 100644 --- a/src/components/CombatLossesPanel.tsx +++ b/src/components/CombatLossesPanel.tsx @@ -18,11 +18,16 @@ interface CombatLossesPanelProps { className?: string } -const LOSS_ITEMS: { key: keyof Omit; label: string; icon: typeof Plane }[] = [ - { key: 'aircraft', label: '战机', icon: Plane }, - { key: 'warships', label: '战舰', icon: Ship }, - { key: 'armor', label: '装甲', icon: Shield }, - { key: 'vehicles', label: '车辆', icon: Car }, +const LOSS_ITEMS: { + key: keyof Omit + label: string + icon: typeof Plane + 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) { @@ -34,14 +39,14 @@ export function CombatLossesPanel({ usLosses, iranLosses, className = '' }: Comb `} >
- + 战损数据
-
- {/* 基地 - 横向第一列 */} -
+
+ {/* 基地 */} +
- + 基地
@@ -63,44 +68,44 @@ export function CombatLossesPanel({ usLosses, iranLosses, className = '' }: Comb
{/* 人员伤亡 */} -
+
- + 人员伤亡
-
+
- - {formatMillions(usLosses.personnelCasualties.killed)} - - {formatMillions(usLosses.personnelCasualties.wounded)} + + {formatMillions(usLosses.personnelCasualties.killed)} + + {formatMillions(usLosses.personnelCasualties.wounded)}
-
+
- - {formatMillions(iranLosses.personnelCasualties.killed)} - - {formatMillions(iranLosses.personnelCasualties.wounded)} + + {formatMillions(iranLosses.personnelCasualties.killed)} + + {formatMillions(iranLosses.personnelCasualties.wounded)}
{/* 战机 / 战舰 / 装甲 / 车辆 */} - {LOSS_ITEMS.map(({ key, label, icon: Icon }) => ( -
+ {LOSS_ITEMS.map(({ key, label, icon: Icon, iconColor }) => ( +
- + {label}
-
+
- {usLosses[key]} + {usLosses[key]}
-
+
- {iranLosses[key]} + {iranLosses[key]}
diff --git a/src/components/ForcePanel.tsx b/src/components/ForcePanel.tsx index a0827c1..a3b1997 100644 --- a/src/components/ForcePanel.tsx +++ b/src/components/ForcePanel.tsx @@ -17,17 +17,17 @@ import type { ReactNode } from 'react' import type { ForceSummary, PowerIndex, ForceAsset } from '@/data/mockData' const ASSET_TYPE_ICONS: Record = { - 航母: , - 驱逐舰: , - 护卫舰: , - 水面舰艇: , - 海军: , - 战机: , - 轰炸机: , - 防空: , - 导弹: , - 无人机: , - 准军事: , + 航母: , + 驱逐舰: , + 护卫舰: , + 水面舰艇: , + 海军: , + 战机: , + 轰炸机: , + 防空: , + 导弹: , + 无人机: , + 准军事: , } interface ForcePanelProps { @@ -84,7 +84,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp label="资产" value={formatMillions(summary.totalAssets)} variant={statVariant} - icon={} + icon={} className="min-w-0" valueSize="base" /> @@ -92,7 +92,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp label="人员" value={formatMillions(summary.personnel)} variant={statVariant} - icon={} + icon={} className="min-w-0" valueSize="base" /> @@ -100,7 +100,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp label="海军" value={formatTenThousands(summary.navalShips)} variant={statVariant} - icon={} + icon={} className="min-w-0" valueSize="lg" /> @@ -108,7 +108,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp label="航空" value={formatTenThousands(summary.aircraft)} variant={statVariant} - icon={} + icon={} className="min-w-0" valueSize="lg" /> @@ -116,7 +116,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp label="无人机" value={formatTenThousands(summary.uav)} variant={statVariant} - icon={} + icon={} className="min-w-0" valueSize="lg" /> @@ -124,7 +124,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp label="导弹消耗" value={formatMillions(summary.missileConsumed)} variant={statVariant} - icon={} + icon={} className="min-w-0" valueSize="base" /> @@ -132,7 +132,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp label="导弹库存" value={formatMillions(summary.missileStock)} variant={statVariant} - icon={} + icon={} className="min-w-0" valueSize="base" /> @@ -141,7 +141,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
- + 综合国力
@@ -161,7 +161,11 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp { key: 'influence', label: '影响力', value: powerIndex.geopoliticalInfluence, icon: Globe }, ].map(({ key, label, value, icon: Icon }) => (
- +
{label} @@ -181,7 +185,7 @@ export function ForcePanel({ side, summary, powerIndex, assets }: ForcePanelProp
- + 主要资产
diff --git a/src/components/WarMap.tsx b/src/components/WarMap.tsx index 040f911..b4a3786 100644 --- a/src/components/WarMap.tsx +++ b/src/components/WarMap.tsx @@ -4,15 +4,40 @@ import type { MapRef } from 'react-map-gl' import type { Map as MapboxMap } from 'mapbox-gl' import 'mapbox-gl/dist/mapbox-gl.css' 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 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 = '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 ALLIES_ADMIN = [ 'Qatar', @@ -33,14 +58,39 @@ const ALLIES_ADMIN = [ // 伊朗攻击源 德黑兰 [lng, lat] const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892] +/** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */ function parabolaPath( start: [number, number], end: [number, number], - height = 2 + height = 3 ): [number, number][] { - const midLng = (start[0] + end[0]) / 2 - const midLat = (start[1] + end[1]) / 2 + height - return [start, [midLng, midLat], end] + const ctrl: [number, number] = [ + (start[0] + end[0]) / 2, + (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' @@ -69,10 +119,16 @@ function toFeature(loc: KeyLoc, side: 'us' | 'iran', status?: BaseStatus) { } } +const FLIGHT_DURATION_MS = 2500 // 光点飞行单程时间 + export function WarMap() { const mapRef = useRef(null) const animRef = useRef(0) const startRef = useRef(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 { usForces, iranForces } = situation @@ -113,20 +169,74 @@ export function WarMap() { } }, [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( () => ({ type: 'FeatureCollection' as const, - features: ATTACKED_TARGETS.map((target) => ({ + features: attackPaths.map((coords) => ({ type: 'Feature' as const, properties: {}, - geometry: { - type: 'LineString' as const, - coordinates: parabolaPath(TEHRAN_SOURCE, target as [number, number]), - }, + geometry: { type: 'LineString' as const, coordinates: coords }, })), }), - [] + [attackPaths] ) const hideNonBelligerentLabels = (map: MapboxMap) => { @@ -147,18 +257,27 @@ export function WarMap() { } } - useEffect(() => { - const map = mapRef.current?.getMap() - if (!map) return + const initAnimation = useRef<(map: MapboxMap) => void>(null!) + initAnimation.current = (map: MapboxMap) => { startRef.current = performance.now() const tick = (t: number) => { const elapsed = t - startRef.current try { - if (map.getLayer('attack-lines')) { - const offset = (elapsed / 16) * 0.8 - map.setPaintProperty('attack-lines', 'line-dasharray', [2, 2]) - map.setPaintProperty('attack-lines', 'line-dash-offset', -offset) + // 光点从起点飞向目标的循环动画 + const src = map.getSource('attack-dots') as { setData: (d: GeoJSON.FeatureCollection) => void } | undefined + const paths = attackPathsRef.current + if (src && paths.length > 0) { + const features: GeoJSON.Feature[] = 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 周期 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-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[] = 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[] = 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[] = 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 (_) {} animRef.current = requestAnimationFrame(tick) } const start = () => { 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) } else { animRef.current = requestAnimationFrame(start) } } - if (map.isStyleLoaded()) start() - else map.once('load', start) + start() + } + useEffect(() => { return () => cancelAnimationFrame(animRef.current) }, []) @@ -215,6 +413,33 @@ export function WarMap() { return (
+ {/* 图例 - 随容器自适应,避免遮挡 */} +
+ + 基地 + + + 遭袭 + + + 海军 + + + 伊朗 + + + 胡塞武装 + + + 林肯打击 + + + 福特打击 + + + 以色列打击 + +
{ + // 地图加载完成后启动动画;延迟确保 Source/Layer 已挂载 + setTimeout(() => initAnimation.current(e.target), 150) + }} > - {/* 美国海军 - 蓝色 */} + {/* 矢量标记:zoom 拉远变小,拉近变大 */} - {/* 美军基地-正常 - 绿色 */} - {/* 美军基地-损毁 - 橙色闪烁 */} - {/* 美军基地-遭袭 - 红点 + 脉冲 */} @@ -284,14 +510,13 @@ export function WarMap() { id="points-attacked-pulse" type="circle" paint={{ - 'circle-radius': 6, + 'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 4, 8, 12, 12, 24], 'circle-color': '#EF4444', 'circle-opacity': 0, }} /> - {/* 伊朗 - 红色 */} + {/* 美以联军打击伊朗:路径线 */} + + + + + + + + + + {/* 林肯号打击光点 */} + ({ + type: 'Feature' as const, + properties: {}, + geometry: { type: 'Point' as const, coordinates: path[0] }, + })), + }} + > + + + + {/* 福特号打击光点 */} + ({ + type: 'Feature' as const, + properties: {}, + geometry: { type: 'Point' as const, coordinates: path[0] }, + })), + }} + > + + + + {/* 以色列打击光点 */} + ({ + type: 'Feature' as const, + properties: {}, + geometry: { type: 'Point' as const, coordinates: path[0] }, + })), + }} + > + + + + {/* 美军打击目标点位 (蓝色) */} + ({ + type: 'Feature' as const, + properties: { name: s.name }, + geometry: { type: 'Point' as const, coordinates: s.coords }, + })), + }} + > + + + + + + {/* 伊朗攻击路径:细线 (矢量) */} + + {/* 光点飞行动画:从德黑兰飞向各目标 */} + ({ + type: 'Feature' as const, + properties: {}, + geometry: { + type: 'Point' as const, + coordinates: path[0], + }, + })), + }} + > + + - {/* 中文标注 */} + {/* 中文标注 - 随 zoom 自适应大小 */} + + + {/* 胡塞武装失控区 - 伊朗红 */} + + + + {/* 胡塞武装标注 */} + + + + + {/* 伊朗标注 */} + + + + {/* 以色列标注 */} + + - {/* 以色列 mesh 高亮 */} + {/* 伊朗区域填充 - 红色系 */} + {/* 以色列区域填充 - 蓝色系 */} + [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[] = [ { name: '阿巴斯港', lat: 27.1832, lng: 56.2666, type: 'Port', region: '伊朗' }, { name: '德黑兰', lat: 35.6892, lng: 51.389, type: 'Capital', region: '伊朗' }, diff --git a/vite.config.ts b/vite.config.ts index ccc85d6..2207152 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,8 +6,20 @@ export default defineConfig({ plugins: [react()], server: { proxy: { - '/api': { target: 'http://localhost:3001', changeOrigin: true }, - '/ws': { target: 'ws://localhost:3001', ws: true }, + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + configure: (proxy) => { + proxy.on('error', () => {}) // 后端未启动时静默失败 + }, + }, + '/ws': { + target: 'ws://localhost:3001', + ws: true, + configure: (proxy) => { + proxy.on('error', () => {}) // 后端未启动时静默失败 + }, + }, }, }, resolve: {