feat: add new file
This commit is contained in:
48
.env.example
48
.env.example
@@ -1,2 +1,46 @@
|
||||
# Get a free token at https://account.mapbox.com/access-tokens/
|
||||
VITE_MAPBOX_ACCESS_TOKEN=pk.eyJ1IjoiZDI5cTAiLCJhIjoiY21oaGRmcTkzMGltZzJscHR1N2FhZnY5dCJ9.7ueF2lS6-C9Mm_xon7NnIA
|
||||
# Mapbox 地图令牌 (波斯湾区域展示)
|
||||
# 免费申请: https://account.mapbox.com/access-tokens/
|
||||
# 复制本文件为 .env 并填入你的 token
|
||||
VITE_MAPBOX_ACCESS_TOKEN=your_mapbox_public_token_here
|
||||
27 个基地完整 JSON 数据
|
||||
[
|
||||
{ "id": 1, "name": "Al Udeid Air Base", "country": "Qatar", "lat": 25.117, "lng": 51.314 },
|
||||
{ "id": 2, "name": "Camp As Sayliyah", "country": "Qatar", "lat": 25.275, "lng": 51.520 },
|
||||
|
||||
{ "id": 3, "name": "Naval Support Activity Bahrain", "country": "Bahrain", "lat": 26.236, "lng": 50.608 },
|
||||
|
||||
{ "id": 4, "name": "Camp Arifjan", "country": "Kuwait", "lat": 28.832, "lng": 47.799 },
|
||||
{ "id": 5, "name": "Ali Al Salem Air Base", "country": "Kuwait", "lat": 29.346, "lng": 47.520 },
|
||||
{ "id": 6, "name": "Camp Buehring", "country": "Kuwait", "lat": 29.603, "lng": 47.456 },
|
||||
|
||||
{ "id": 7, "name": "Al Dhafra Air Base", "country": "UAE", "lat": 24.248, "lng": 54.547 },
|
||||
|
||||
{ "id": 8, "name": "Prince Sultan Air Base", "country": "Saudi Arabia", "lat": 24.062, "lng": 47.580 },
|
||||
{ "id": 9, "name": "Eskan Village", "country": "Saudi Arabia", "lat": 24.774, "lng": 46.738 },
|
||||
|
||||
{ "id": 10, "name": "Al Asad Airbase", "country": "Iraq", "lat": 33.785, "lng": 42.441 },
|
||||
{ "id": 11, "name": "Erbil Air Base", "country": "Iraq", "lat": 36.237, "lng": 43.963 },
|
||||
{ "id": 12, "name": "Baghdad Diplomatic Support Center", "country": "Iraq", "lat": 33.315, "lng": 44.366 },
|
||||
{ "id": 13, "name": "Camp Taji", "country": "Iraq", "lat": 33.556, "lng": 44.256 },
|
||||
{ "id": 14, "name": "Ain al-Asad", "country": "Iraq", "lat": 33.800, "lng": 42.450 },
|
||||
|
||||
{ "id": 15, "name": "Al-Tanf Garrison", "country": "Syria", "lat": 33.490, "lng": 38.618 },
|
||||
{ "id": 16, "name": "Rmelan Landing Zone", "country": "Syria", "lat": 37.015, "lng": 41.885 },
|
||||
{ "id": 17, "name": "Shaddadi Base", "country": "Syria", "lat": 36.058, "lng": 40.730 },
|
||||
{ "id": 18, "name": "Conoco Gas Field Base", "country": "Syria", "lat": 35.336, "lng": 40.295 },
|
||||
|
||||
{ "id": 19, "name": "Muwaffaq Salti Air Base", "country": "Jordan", "lat": 32.356, "lng": 36.259 },
|
||||
|
||||
{ "id": 20, "name": "Incirlik Air Base", "country": "Turkey", "lat": 37.002, "lng": 35.425 },
|
||||
{ "id": 21, "name": "Kurecik Radar Station", "country": "Turkey", "lat": 38.354, "lng": 37.794 },
|
||||
|
||||
{ "id": 22, "name": "Nevatim Air Base", "country": "Israel", "lat": 31.208, "lng": 35.012 },
|
||||
{ "id": 23, "name": "Ramon Air Base", "country": "Israel", "lat": 30.776, "lng": 34.666 },
|
||||
|
||||
{ "id": 24, "name": "Thumrait Air Base", "country": "Oman", "lat": 17.666, "lng": 54.024 },
|
||||
{ "id": 25, "name": "Masirah Air Base", "country": "Oman", "lat": 20.675, "lng": 58.890 },
|
||||
|
||||
{ "id": 26, "name": "West Cairo Air Base", "country": "Egypt", "lat": 30.915, "lng": 30.298 },
|
||||
|
||||
{ "id": 27, "name": "Camp Lemonnier", "country": "Djibouti", "lat": 11.547, "lng": 43.159 }
|
||||
]
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,6 +23,9 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# API database
|
||||
server/data.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
|
||||
36
README.md
36
README.md
@@ -29,6 +29,30 @@ cp .env.example .env
|
||||
|
||||
Without a token, the map area shows a placeholder with location labels.
|
||||
|
||||
### API 与数据库
|
||||
|
||||
页面数据通过 REST API 从后端获取,后端使用 SQLite 存储。
|
||||
|
||||
```bash
|
||||
# 首次运行:初始化数据库并写入种子数据
|
||||
npm run api:seed
|
||||
|
||||
# 启动 API 服务(默认 http://localhost:3001)
|
||||
npm run api
|
||||
```
|
||||
|
||||
开发时需同时运行前端与 API:
|
||||
|
||||
```bash
|
||||
# 终端 1
|
||||
npm run api
|
||||
|
||||
# 终端 2
|
||||
npm run dev
|
||||
```
|
||||
|
||||
API 会由 Vite 代理到 `/api`,前端通过 `/api/situation` 获取完整态势数据。数据库文件位于 `server/data.db`,可通过修改表数据实现动态调整。
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
@@ -50,12 +74,20 @@ src/
|
||||
│ ├── ForcePanel.tsx # Reusable left/right panel for military forces
|
||||
│ ├── WarMap.tsx # Mapbox GL (Persian Gulf center)
|
||||
│ └── StatCard.tsx # Reusable number card
|
||||
├── api/
|
||||
│ └── situation.ts # 态势数据 API 请求
|
||||
├── store/
|
||||
│ └── situationStore.ts # Zustand store + WebSocket mock logic
|
||||
│ └── situationStore.ts # Zustand store + API 轮询
|
||||
├── data/
|
||||
│ └── mockData.ts # TypeScript interfaces & initial mock data
|
||||
│ └── mockData.ts # 类型定义与初始兜底数据
|
||||
├── pages/
|
||||
│ └── Dashboard.tsx # Main layout (1920×1080)
|
||||
├── App.tsx
|
||||
└── index.css
|
||||
server/
|
||||
├── index.js # Express API 入口
|
||||
├── db.js # SQLite 建表与连接
|
||||
├── routes.js # /api/situation 路由
|
||||
├── seed.js # 数据库种子脚本
|
||||
└── data.db # SQLite 数据库(运行 seed 后生成)
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/usa_logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>美伊军事态势显示</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
||||
284
map.md
Normal file
284
map.md
Normal file
@@ -0,0 +1,284 @@
|
||||
|
||||
[
|
||||
{
|
||||
"name": "USS Abraham Lincoln CSG",
|
||||
"type": "Aircraft Carrier Strike Group",
|
||||
"lat": 12.5,
|
||||
"lng": 50.0,
|
||||
"region": "Arabian Sea SE of Socotra"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Destroyer (Gulf of Oman)",
|
||||
"type": "Destroyer",
|
||||
"lat": 25.2,
|
||||
"lng": 58.0,
|
||||
"region": "Gulf of Oman"
|
||||
},
|
||||
{
|
||||
"name": "Coast Guard Ship 1 (Gulf of Oman)",
|
||||
"type": "Coast Guard",
|
||||
"lat": 25.4,
|
||||
"lng": 58.2,
|
||||
"region": "Gulf of Oman"
|
||||
},
|
||||
{
|
||||
"name": "Coast Guard Ship 2 (Gulf of Oman)",
|
||||
"type": "Coast Guard",
|
||||
"lat": 25.0,
|
||||
"lng": 57.8,
|
||||
"region": "Gulf of Oman"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Destroyer (Northern Persian Gulf)",
|
||||
"type": "Destroyer",
|
||||
"lat": 26.5,
|
||||
"lng": 51.0,
|
||||
"region": "Persian Gulf"
|
||||
},
|
||||
{
|
||||
"name": "Frigate 1 (Persian Gulf)",
|
||||
"type": "Frigate",
|
||||
"lat": 26.7,
|
||||
"lng": 50.6,
|
||||
"region": "Persian Gulf"
|
||||
},
|
||||
{
|
||||
"name": "Frigate 2 (Persian Gulf)",
|
||||
"type": "Frigate",
|
||||
"lat": 27.0,
|
||||
"lng": 50.2,
|
||||
"region": "Persian Gulf"
|
||||
},
|
||||
{
|
||||
"name": "Frigate 3 (Persian Gulf)",
|
||||
"type": "Frigate",
|
||||
"lat": 26.3,
|
||||
"lng": 50.8,
|
||||
"region": "Persian Gulf"
|
||||
},
|
||||
{
|
||||
"name": "Auxiliary Ship 1",
|
||||
"type": "Auxiliary",
|
||||
"lat": 26.0,
|
||||
"lng": 51.2,
|
||||
"region": "Persian Gulf"
|
||||
},
|
||||
{
|
||||
"name": "Auxiliary Ship 2",
|
||||
"type": "Auxiliary",
|
||||
"lat": 25.8,
|
||||
"lng": 51.5,
|
||||
"region": "Persian Gulf"
|
||||
},
|
||||
{
|
||||
"name": "Auxiliary Ship 3",
|
||||
"type": "Auxiliary",
|
||||
"lat": 26.2,
|
||||
"lng": 50.9,
|
||||
"region": "Persian Gulf"
|
||||
}
|
||||
]
|
||||
|
||||
卡塔尔
|
||||
|
||||
Al Udeid Air Base
|
||||
|
||||
Camp As Sayliyah
|
||||
|
||||
🇧🇭 巴林
|
||||
|
||||
Naval Support Activity Bahrain
|
||||
|
||||
🇰🇼 科威特
|
||||
|
||||
Camp Arifjan
|
||||
|
||||
Ali Al Salem Air Base
|
||||
|
||||
Camp Buehring
|
||||
|
||||
🇦🇪 阿联酋
|
||||
|
||||
Al Dhafra Air Base
|
||||
|
||||
🇸🇦 沙特阿拉伯
|
||||
|
||||
Prince Sultan Air Base
|
||||
|
||||
Eskan Village
|
||||
|
||||
🇮🇶 伊拉克
|
||||
|
||||
Al Asad Airbase
|
||||
|
||||
Erbil Air Base
|
||||
|
||||
Baghdad Diplomatic Support Center
|
||||
|
||||
Camp Taji
|
||||
|
||||
Ain al-Asad (部分文献与 Al Asad 同义但可单列展示)
|
||||
|
||||
🇸🇾 叙利亚(驻点)
|
||||
|
||||
Al-Tanf Garrison
|
||||
|
||||
Rmelan Landing Zone
|
||||
|
||||
Shaddadi Base
|
||||
|
||||
Conoco Gas Field Base
|
||||
|
||||
🇯🇴 约旦
|
||||
|
||||
Muwaffaq Salti Air Base
|
||||
|
||||
🇹🇷 土耳其
|
||||
|
||||
Incirlik Air Base
|
||||
|
||||
Kürecik Radar Station
|
||||
|
||||
🇮🇱 以色列(合作设施)
|
||||
|
||||
Nevatim Air Base (美军设施区)
|
||||
|
||||
Ramon Air Base (协作区)
|
||||
|
||||
🇴🇲 阿曼
|
||||
|
||||
Thumrait Air Base
|
||||
|
||||
Masirah Air Base
|
||||
|
||||
🇪🇬 埃及(区域支援)
|
||||
|
||||
West Cairo Air Base
|
||||
|
||||
🇩🇯 吉布提(非波斯湾但中东行动关键)
|
||||
|
||||
地图基础设置
|
||||
|
||||
中心:
|
||||
|
||||
center: [52.5, 26.5]
|
||||
zoom: 5.2
|
||||
style: "mapbox://styles/mapbox/dark-v11"
|
||||
二、27 个基地完整 JSON 数据
|
||||
[
|
||||
{ "id": 1, "name": "Al Udeid Air Base", "country": "Qatar", "lat": 25.117, "lng": 51.314 },
|
||||
{ "id": 2, "name": "Camp As Sayliyah", "country": "Qatar", "lat": 25.275, "lng": 51.520 },
|
||||
|
||||
{ "id": 3, "name": "Naval Support Activity Bahrain", "country": "Bahrain", "lat": 26.236, "lng": 50.608 },
|
||||
|
||||
{ "id": 4, "name": "Camp Arifjan", "country": "Kuwait", "lat": 28.832, "lng": 47.799 },
|
||||
{ "id": 5, "name": "Ali Al Salem Air Base", "country": "Kuwait", "lat": 29.346, "lng": 47.520 },
|
||||
{ "id": 6, "name": "Camp Buehring", "country": "Kuwait", "lat": 29.603, "lng": 47.456 },
|
||||
|
||||
{ "id": 7, "name": "Al Dhafra Air Base", "country": "UAE", "lat": 24.248, "lng": 54.547 },
|
||||
|
||||
{ "id": 8, "name": "Prince Sultan Air Base", "country": "Saudi Arabia", "lat": 24.062, "lng": 47.580 },
|
||||
{ "id": 9, "name": "Eskan Village", "country": "Saudi Arabia", "lat": 24.774, "lng": 46.738 },
|
||||
|
||||
{ "id": 10, "name": "Al Asad Airbase", "country": "Iraq", "lat": 33.785, "lng": 42.441 },
|
||||
{ "id": 11, "name": "Erbil Air Base", "country": "Iraq", "lat": 36.237, "lng": 43.963 },
|
||||
{ "id": 12, "name": "Baghdad Diplomatic Support Center", "country": "Iraq", "lat": 33.315, "lng": 44.366 },
|
||||
{ "id": 13, "name": "Camp Taji", "country": "Iraq", "lat": 33.556, "lng": 44.256 },
|
||||
{ "id": 14, "name": "Ain al-Asad", "country": "Iraq", "lat": 33.800, "lng": 42.450 },
|
||||
|
||||
{ "id": 15, "name": "Al-Tanf Garrison", "country": "Syria", "lat": 33.490, "lng": 38.618 },
|
||||
{ "id": 16, "name": "Rmelan Landing Zone", "country": "Syria", "lat": 37.015, "lng": 41.885 },
|
||||
{ "id": 17, "name": "Shaddadi Base", "country": "Syria", "lat": 36.058, "lng": 40.730 },
|
||||
{ "id": 18, "name": "Conoco Gas Field Base", "country": "Syria", "lat": 35.336, "lng": 40.295 },
|
||||
|
||||
{ "id": 19, "name": "Muwaffaq Salti Air Base", "country": "Jordan", "lat": 32.356, "lng": 36.259 },
|
||||
|
||||
{ "id": 20, "name": "Incirlik Air Base", "country": "Turkey", "lat": 37.002, "lng": 35.425 },
|
||||
{ "id": 21, "name": "Kurecik Radar Station", "country": "Turkey", "lat": 38.354, "lng": 37.794 },
|
||||
|
||||
{ "id": 22, "name": "Nevatim Air Base", "country": "Israel", "lat": 31.208, "lng": 35.012 },
|
||||
{ "id": 23, "name": "Ramon Air Base", "country": "Israel", "lat": 30.776, "lng": 34.666 },
|
||||
|
||||
{ "id": 24, "name": "Thumrait Air Base", "country": "Oman", "lat": 17.666, "lng": 54.024 },
|
||||
{ "id": 25, "name": "Masirah Air Base", "country": "Oman", "lat": 20.675, "lng": 58.890 },
|
||||
|
||||
{ "id": 26, "name": "West Cairo Air Base", "country": "Egypt", "lat": 30.915, "lng": 30.298 },
|
||||
|
||||
{ "id": 27, "name": "Camp Lemonnier", "country": "Djibouti", "lat": 11.547, "lng": 43.159 }
|
||||
]
|
||||
三、状态模型
|
||||
type BaseStatus = "operational" | "damaged" | "attacked"
|
||||
|
||||
interface BaseAttackStatus {
|
||||
id: number
|
||||
status: BaseStatus
|
||||
damageLevel: 1 | 2 | 3
|
||||
attackTime?: number
|
||||
}
|
||||
四、可视化规则
|
||||
1️⃣ 状态颜色
|
||||
|
||||
operational → 绿色稳定点
|
||||
|
||||
damaged → 橙色闪烁
|
||||
|
||||
attacked → 红色脉冲扩散动画
|
||||
|
||||
2️⃣ 红色脉冲动画
|
||||
|
||||
2 秒循环
|
||||
|
||||
扩散半径 0 → 40px
|
||||
|
||||
opacity 1 → 0
|
||||
|
||||
3️⃣ 伊朗攻击源
|
||||
|
||||
固定发射源坐标:
|
||||
|
||||
const IRAN_SOURCE = [51.3890, 35.6892] // Tehran
|
||||
|
||||
攻击动画使用抛物线 LineLayer:
|
||||
|
||||
颜色:#ff0000
|
||||
|
||||
线宽:2px
|
||||
|
||||
飞行时间:1200ms
|
||||
|
||||
五、大屏右侧态势面板
|
||||
|
||||
显示:
|
||||
|
||||
总基地数:27
|
||||
|
||||
被袭击:X
|
||||
|
||||
严重损毁:X
|
||||
|
||||
中度损毁:X
|
||||
|
||||
轻度损毁:X
|
||||
|
||||
每 5 秒自动刷新统计。
|
||||
|
||||
六、性能要求
|
||||
|
||||
使用 requestAnimationFrame
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
禁止重复创建 marker
|
||||
|
||||
所有动画走 WebGL 图层
|
||||
|
||||
禁止 DOM 动画
|
||||
1182
package-lock.json
generated
1182
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,19 +5,25 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"api": "node server/index.js",
|
||||
"api:seed": "node server/seed.js",
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"express": "^4.21.1",
|
||||
"lucide-react": "^0.460.0",
|
||||
"mapbox-gl": "^3.6.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-map-gl": "^7.1.7",
|
||||
"ws": "^8.19.0",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
107
server/db.js
Normal file
107
server/db.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const Database = require('better-sqlite3')
|
||||
const path = require('path')
|
||||
|
||||
const dbPath = path.join(__dirname, 'data.db')
|
||||
const db = new Database(dbPath)
|
||||
|
||||
// 启用外键
|
||||
db.pragma('journal_mode = WAL')
|
||||
|
||||
// 建表
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS situation (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
data TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS force_summary (
|
||||
side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')),
|
||||
total_assets INTEGER NOT NULL,
|
||||
personnel INTEGER NOT NULL,
|
||||
naval_ships INTEGER NOT NULL,
|
||||
aircraft INTEGER NOT NULL,
|
||||
ground_units INTEGER NOT NULL,
|
||||
uav INTEGER NOT NULL,
|
||||
missile_consumed INTEGER NOT NULL,
|
||||
missile_stock INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS power_index (
|
||||
side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')),
|
||||
overall INTEGER NOT NULL,
|
||||
military_strength INTEGER NOT NULL,
|
||||
economic_power INTEGER NOT NULL,
|
||||
geopolitical_influence INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS force_asset (
|
||||
id TEXT PRIMARY KEY,
|
||||
side TEXT NOT NULL CHECK (side IN ('us', 'iran')),
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
count INTEGER NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('active', 'standby', 'alert')),
|
||||
lat REAL,
|
||||
lng REAL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS key_location (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
side TEXT NOT NULL CHECK (side IN ('us', 'iran')),
|
||||
name TEXT NOT NULL,
|
||||
lat REAL NOT NULL,
|
||||
lng REAL NOT NULL,
|
||||
type TEXT,
|
||||
region TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS combat_losses (
|
||||
side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')),
|
||||
bases_destroyed INTEGER NOT NULL,
|
||||
bases_damaged INTEGER NOT NULL,
|
||||
personnel_killed INTEGER NOT NULL,
|
||||
personnel_wounded INTEGER NOT NULL,
|
||||
aircraft INTEGER NOT NULL,
|
||||
warships INTEGER NOT NULL,
|
||||
armor INTEGER NOT NULL,
|
||||
vehicles INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wall_street_trend (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
time TEXT NOT NULL,
|
||||
value INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS retaliation_current (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
value INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS retaliation_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
time TEXT NOT NULL,
|
||||
value INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS situation_update (
|
||||
id TEXT PRIMARY KEY,
|
||||
timestamp TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
severity TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
// 迁移:为已有 key_location 表添加 type、region、status、damage_level 列
|
||||
try {
|
||||
const cols = db.prepare('PRAGMA table_info(key_location)').all()
|
||||
const names = cols.map((c) => c.name)
|
||||
if (!names.includes('type')) db.exec('ALTER TABLE key_location ADD COLUMN type TEXT')
|
||||
if (!names.includes('region')) db.exec('ALTER TABLE key_location ADD COLUMN region TEXT')
|
||||
if (!names.includes('status')) db.exec('ALTER TABLE key_location ADD COLUMN status TEXT DEFAULT "operational"')
|
||||
if (!names.includes('damage_level')) db.exec('ALTER TABLE key_location ADD COLUMN damage_level INTEGER')
|
||||
} catch (_) {}
|
||||
|
||||
module.exports = db
|
||||
34
server/index.js
Normal file
34
server/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const http = require('http')
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const { WebSocketServer } = require('ws')
|
||||
const routes = require('./routes')
|
||||
const { getSituation } = require('./situationData')
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.API_PORT || 3001
|
||||
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
app.use('/api', routes)
|
||||
app.get('/api/health', (_, res) => res.json({ ok: true }))
|
||||
|
||||
const server = http.createServer(app)
|
||||
|
||||
const wss = new WebSocketServer({ server, path: '/ws' })
|
||||
wss.on('connection', (ws) => {
|
||||
ws.send(JSON.stringify({ type: 'situation', data: getSituation() }))
|
||||
})
|
||||
function broadcastSituation() {
|
||||
try {
|
||||
const data = JSON.stringify({ type: 'situation', data: getSituation() })
|
||||
wss.clients.forEach((c) => {
|
||||
if (c.readyState === 1) c.send(data)
|
||||
})
|
||||
} catch (_) {}
|
||||
}
|
||||
setInterval(broadcastSituation, 5000)
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`API + WebSocket running at http://localhost:${PORT}`)
|
||||
})
|
||||
5
server/package.json
Normal file
5
server/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "usa-api",
|
||||
"private": true,
|
||||
"type": "commonjs"
|
||||
}
|
||||
15
server/routes.js
Normal file
15
server/routes.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const express = require('express')
|
||||
const { getSituation } = require('./situationData')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/situation', (req, res) => {
|
||||
try {
|
||||
res.json(getSituation())
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({ error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
174
server/seed.js
Normal file
174
server/seed.js
Normal file
@@ -0,0 +1,174 @@
|
||||
const db = require('./db')
|
||||
|
||||
// 与 src/data/mapLocations.ts 同步:62 基地,27 被袭 (严重6 中度12 轻度9)
|
||||
function getUsLocations() {
|
||||
const naval = [
|
||||
{ name: '林肯号航母 (CVN-72)', lat: 24.1568, lng: 58.4215, type: 'Aircraft Carrier', region: '北阿拉伯海', status: 'operational', damage_level: null },
|
||||
{ name: '福特号航母 (CVN-78)', lat: 35.7397, lng: 24.1002, type: 'Aircraft Carrier', region: '东地中海', status: 'operational', damage_level: null },
|
||||
{ name: '驱逐舰(阿曼湾)', lat: 25.2, lng: 58.0, type: 'Destroyer', region: '阿曼湾', status: 'operational', damage_level: null },
|
||||
{ name: '海岸警卫队 1', lat: 25.4, lng: 58.2, type: 'Coast Guard', region: '阿曼湾', status: 'operational', damage_level: null },
|
||||
{ name: '海岸警卫队 2', lat: 25.0, lng: 57.8, type: 'Coast Guard', region: '阿曼湾', status: 'operational', damage_level: null },
|
||||
{ name: '驱逐舰(波斯湾北部)', lat: 26.5, lng: 51.0, type: 'Destroyer', region: '波斯湾', status: 'operational', damage_level: null },
|
||||
{ name: '护卫舰 1', lat: 26.7, lng: 50.6, type: 'Frigate', region: '波斯湾', status: 'operational', damage_level: null },
|
||||
{ name: '护卫舰 2', lat: 27.0, lng: 50.2, type: 'Frigate', region: '波斯湾', status: 'operational', damage_level: null },
|
||||
{ name: '护卫舰 3', lat: 26.3, lng: 50.8, type: 'Frigate', region: '波斯湾', status: 'operational', damage_level: null },
|
||||
{ name: '辅助舰 1', lat: 26.0, lng: 51.2, type: 'Auxiliary', region: '波斯湾', status: 'operational', damage_level: null },
|
||||
{ name: '辅助舰 2', lat: 25.8, lng: 51.5, type: 'Auxiliary', region: '波斯湾', status: 'operational', damage_level: null },
|
||||
{ name: '辅助舰 3', lat: 26.2, lng: 50.9, type: 'Auxiliary', region: '波斯湾', status: 'operational', damage_level: null },
|
||||
]
|
||||
const attacked = [
|
||||
{ name: '阿萨德空军基地', lat: 33.785, lng: 42.441, type: 'Base', region: '伊拉克', status: 'attacked', damage_level: 3 },
|
||||
{ name: '巴格达外交支援中心', lat: 33.315, lng: 44.366, type: 'Base', region: '伊拉克', status: 'attacked', damage_level: 3 },
|
||||
{ name: '乌代德空军基地', lat: 25.117, lng: 51.314, type: 'Base', region: '卡塔尔', status: 'attacked', damage_level: 3 },
|
||||
{ name: '埃尔比勒空军基地', lat: 36.237, lng: 43.963, type: 'Base', region: '伊拉克', status: 'attacked', damage_level: 3 },
|
||||
{ name: '因吉尔利克空军基地', lat: 37.002, lng: 35.425, type: 'Base', region: '土耳其', status: 'attacked', damage_level: 3 },
|
||||
{ name: '苏尔坦亲王空军基地', lat: 24.062, lng: 47.58, type: 'Base', region: '沙特', status: 'attacked', damage_level: 3 },
|
||||
{ name: '塔吉军营', lat: 33.556, lng: 44.256, type: 'Base', region: '伊拉克', status: 'attacked', damage_level: 2 },
|
||||
{ name: '阿因·阿萨德', lat: 33.8, lng: 42.45, type: 'Base', region: '伊拉克', status: 'attacked', damage_level: 2 },
|
||||
{ name: '坦夫驻军', lat: 33.49, lng: 38.618, type: 'Base', region: '叙利亚', status: 'attacked', damage_level: 2 },
|
||||
{ name: '沙达迪基地', lat: 36.058, lng: 40.73, type: 'Base', region: '叙利亚', status: 'attacked', damage_level: 2 },
|
||||
{ name: '康诺克气田基地', lat: 35.336, lng: 40.295, type: 'Base', region: '叙利亚', status: 'attacked', damage_level: 2 },
|
||||
{ name: '尔梅兰着陆区', lat: 37.015, lng: 41.885, type: 'Base', region: '叙利亚', status: 'attacked', damage_level: 2 },
|
||||
{ name: '阿里夫坚军营', lat: 28.832, lng: 47.799, type: 'Base', region: '科威特', status: 'attacked', damage_level: 2 },
|
||||
{ name: '阿里·萨勒姆空军基地', lat: 29.346, lng: 47.52, type: 'Base', region: '科威特', status: 'attacked', damage_level: 2 },
|
||||
{ name: '巴林海军支援站', lat: 26.236, lng: 50.608, type: 'Base', region: '巴林', status: 'attacked', damage_level: 2 },
|
||||
{ name: '达夫拉空军基地', lat: 24.248, lng: 54.547, type: 'Base', region: '阿联酋', status: 'attacked', damage_level: 2 },
|
||||
{ name: '埃斯康村', lat: 24.774, lng: 46.738, type: 'Base', region: '沙特', status: 'attacked', damage_level: 2 },
|
||||
{ name: '内瓦提姆空军基地', lat: 31.208, lng: 35.012, type: 'Base', region: '以色列', status: 'attacked', damage_level: 2 },
|
||||
{ name: '布林军营', lat: 29.603, lng: 47.456, type: 'Base', region: '科威特', status: 'attacked', damage_level: 1 },
|
||||
{ name: '赛利耶军营', lat: 25.275, lng: 51.52, type: 'Base', region: '卡塔尔', status: 'attacked', damage_level: 1 },
|
||||
{ name: '拉蒙空军基地', lat: 30.776, lng: 34.666, type: 'Base', region: '以色列', status: 'attacked', damage_level: 1 },
|
||||
{ name: '穆瓦法克·萨尔蒂空军基地', lat: 32.356, lng: 36.259, type: 'Base', region: '约旦', status: 'attacked', damage_level: 1 },
|
||||
{ name: '屈雷吉克雷达站', lat: 38.354, lng: 37.794, type: 'Base', region: '土耳其', status: 'attacked', damage_level: 1 },
|
||||
{ name: '苏姆莱特空军基地', lat: 17.666, lng: 54.024, type: 'Base', region: '阿曼', status: 'attacked', damage_level: 1 },
|
||||
{ name: '马西拉空军基地', lat: 20.675, lng: 58.89, type: 'Base', region: '阿曼', status: 'attacked', damage_level: 1 },
|
||||
{ name: '西开罗空军基地', lat: 30.915, lng: 30.298, type: 'Base', region: '埃及', status: 'attacked', damage_level: 1 },
|
||||
{ name: '勒莫尼耶军营', lat: 11.547, lng: 43.159, type: 'Base', region: '吉布提', status: 'attacked', damage_level: 1 },
|
||||
]
|
||||
const newBases = [
|
||||
{ name: '多哈后勤中心', lat: 25.29, lng: 51.53, type: 'Base', region: '卡塔尔', status: 'operational', damage_level: null },
|
||||
{ name: '贾法勒海军站', lat: 26.22, lng: 50.62, type: 'Base', region: '巴林', status: 'operational', damage_level: null },
|
||||
{ name: '阿兹祖尔前方作战点', lat: 29.45, lng: 47.9, type: 'Base', region: '科威特', status: 'operational', damage_level: null },
|
||||
{ name: '艾哈迈迪后勤枢纽', lat: 29.08, lng: 48.09, type: 'Base', region: '科威特', status: 'operational', damage_level: null },
|
||||
{ name: '富查伊拉港站', lat: 25.13, lng: 56.35, type: 'Base', region: '阿联酋', status: 'operational', damage_level: null },
|
||||
{ name: '哈伊马角前方点', lat: 25.79, lng: 55.94, type: 'Base', region: '阿联酋', status: 'operational', damage_level: null },
|
||||
{ name: '利雅得联络站', lat: 24.71, lng: 46.68, type: 'Base', region: '沙特', status: 'operational', damage_level: null },
|
||||
{ name: '朱拜勒港支援点', lat: 27.0, lng: 49.65, type: 'Base', region: '沙特', status: 'operational', damage_level: null },
|
||||
{ name: '塔布克空军前哨', lat: 28.38, lng: 36.6, type: 'Base', region: '沙特', status: 'operational', damage_level: null },
|
||||
{ name: '拜莱德空军基地', lat: 33.94, lng: 44.36, type: 'Base', region: '伊拉克', status: 'operational', damage_level: null },
|
||||
{ name: '巴士拉后勤站', lat: 30.5, lng: 47.78, type: 'Base', region: '伊拉克', status: 'operational', damage_level: null },
|
||||
{ name: '基尔库克前哨', lat: 35.47, lng: 44.35, type: 'Base', region: '伊拉克', status: 'operational', damage_level: null },
|
||||
{ name: '摩苏尔支援点', lat: 36.34, lng: 43.14, type: 'Base', region: '伊拉克', status: 'operational', damage_level: null },
|
||||
{ name: '哈塞克联络站', lat: 36.5, lng: 40.75, type: 'Base', region: '叙利亚', status: 'operational', damage_level: null },
|
||||
{ name: '代尔祖尔前哨', lat: 35.33, lng: 40.14, type: 'Base', region: '叙利亚', status: 'operational', damage_level: null },
|
||||
{ name: '安曼协调中心', lat: 31.95, lng: 35.93, type: 'Base', region: '约旦', status: 'operational', damage_level: null },
|
||||
{ name: '伊兹密尔支援站', lat: 38.42, lng: 27.14, type: 'Base', region: '土耳其', status: 'operational', damage_level: null },
|
||||
{ name: '哈泽瑞姆空军基地', lat: 31.07, lng: 34.84, type: 'Base', region: '以色列', status: 'operational', damage_level: null },
|
||||
{ name: '杜古姆港站', lat: 19.66, lng: 57.76, type: 'Base', region: '阿曼', status: 'operational', damage_level: null },
|
||||
{ name: '塞拉莱前方点', lat: 17.01, lng: 54.1, type: 'Base', region: '阿曼', status: 'operational', damage_level: null },
|
||||
{ name: '亚历山大港联络站', lat: 31.2, lng: 29.9, type: 'Base', region: '埃及', status: 'operational', damage_level: null },
|
||||
{ name: '卢克索前哨', lat: 25.69, lng: 32.64, type: 'Base', region: '埃及', status: 'operational', damage_level: null },
|
||||
{ name: '吉布提港支援点', lat: 11.59, lng: 43.15, type: 'Base', region: '吉布提', status: 'operational', damage_level: null },
|
||||
{ name: '卡塔尔应急医疗站', lat: 25.22, lng: 51.45, type: 'Base', region: '卡塔尔', status: 'operational', damage_level: null },
|
||||
{ name: '沙特哈立德国王基地', lat: 24.96, lng: 46.7, type: 'Base', region: '沙特', status: 'operational', damage_level: null },
|
||||
{ name: '伊拉克巴拉德联勤站', lat: 33.75, lng: 44.25, type: 'Base', region: '伊拉克', status: 'operational', damage_level: null },
|
||||
{ name: '叙利亚奥马尔油田站', lat: 36.22, lng: 40.45, type: 'Base', region: '叙利亚', status: 'operational', damage_level: null },
|
||||
{ name: '约旦侯赛因国王基地', lat: 31.72, lng: 36.01, type: 'Base', region: '约旦', status: 'operational', damage_level: null },
|
||||
{ name: '土耳其巴特曼站', lat: 37.88, lng: 41.13, type: 'Base', region: '土耳其', status: 'operational', damage_level: null },
|
||||
{ name: '以色列帕尔马欣站', lat: 31.9, lng: 34.95, type: 'Base', region: '以色列', status: 'operational', damage_level: null },
|
||||
{ name: '阿曼杜古姆扩建点', lat: 19.55, lng: 57.8, type: 'Base', region: '阿曼', status: 'operational', damage_level: null },
|
||||
{ name: '埃及纳特龙湖站', lat: 30.37, lng: 30.2, type: 'Base', region: '埃及', status: 'operational', damage_level: null },
|
||||
{ name: '吉布提查贝尔达站', lat: 11.73, lng: 42.9, type: 'Base', region: '吉布提', status: 'operational', damage_level: null },
|
||||
{ name: '阿联酋迪拜港联络', lat: 25.27, lng: 55.3, type: 'Base', region: '阿联酋', status: 'operational', damage_level: null },
|
||||
{ name: '伊拉克尼尼微前哨', lat: 36.22, lng: 43.1, type: 'Base', region: '伊拉克', status: 'operational', damage_level: null },
|
||||
]
|
||||
return [...naval, ...attacked, ...newBases]
|
||||
}
|
||||
|
||||
function seed() {
|
||||
db.exec(`
|
||||
INSERT OR REPLACE INTO force_summary (side, total_assets, personnel, naval_ships, aircraft, ground_units, uav, missile_consumed, missile_stock) VALUES
|
||||
('us', 1245, 185000, 292, 1862, 18, 418, 1056, 2840),
|
||||
('iran', 7850, 2350000, 4250, 8200, 350, 750, 3720, 13800);
|
||||
`)
|
||||
|
||||
db.exec(`
|
||||
INSERT OR REPLACE INTO power_index (side, overall, military_strength, economic_power, geopolitical_influence) VALUES
|
||||
('us', 94, 96, 98, 97),
|
||||
('iran', 42, 58, 28, 35);
|
||||
`)
|
||||
|
||||
const insertAsset = db.prepare(`
|
||||
INSERT OR REPLACE INTO force_asset (id, side, name, type, count, status, lat, lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
const usAssets = [
|
||||
['us-1', 'us', '双航母打击群 (CVN-72 & CVN-78)', '航母', 2, 'active', null, null],
|
||||
['us-2', 'us', '阿利·伯克级驱逐舰', '驱逐舰', 4, 'active', null, null],
|
||||
['us-3', 'us', 'F/A-18 中队', '战机', 48, 'active', null, null],
|
||||
['us-4', 'us', 'F-35 中队', '战机', 48, 'active', null, null],
|
||||
['us-5', 'us', 'F-22 猛禽', '战机', 12, 'active', null, null],
|
||||
['us-6', 'us', 'B-2 幽灵', '轰炸机', 2, 'alert', null, null],
|
||||
['us-7', 'us', '爱国者防空系统', '防空', 3, 'active', null, null],
|
||||
['us-8', 'us', 'MQ-9 死神', '无人机', 28, 'active', null, null],
|
||||
['us-9', 'us', 'MQ-1C 灰鹰', '无人机', 45, 'active', null, null],
|
||||
]
|
||||
const iranAssets = [
|
||||
['ir-1', 'iran', '护卫舰', '水面舰艇', 6, 'active', null, null],
|
||||
['ir-2', 'iran', '快攻艇', '海军', 100, 'active', null, null],
|
||||
['ir-3', 'iran', 'F-4 Phantom', '战机', 62, 'standby', null, null],
|
||||
['ir-4', 'iran', 'F-14 Tomcat', '战机', 24, 'active', null, null],
|
||||
['ir-5', 'iran', '弹道导弹', '导弹', 3400, 'alert', null, null],
|
||||
['ir-6', 'iran', '伊斯兰革命卫队海军', '准军事', 25000, 'active', null, null],
|
||||
['ir-7', 'iran', '沙希德-136', '无人机', 750, 'alert', null, null],
|
||||
['ir-8', 'iran', '法塔赫 (Fattah)', '导弹', 12, 'alert', null, null],
|
||||
['ir-9', 'iran', '穆哈杰-6', '无人机', 280, 'active', null, null],
|
||||
]
|
||||
;[...usAssets, ...iranAssets].forEach((row) => insertAsset.run(...row))
|
||||
|
||||
const insertLoc = db.prepare(`
|
||||
INSERT INTO key_location (side, name, lat, lng, type, region, status, damage_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
db.exec('DELETE FROM key_location')
|
||||
|
||||
for (const loc of getUsLocations()) {
|
||||
insertLoc.run('us', loc.name, loc.lat, loc.lng, loc.type, loc.region, loc.status, loc.damage_level)
|
||||
}
|
||||
const iranLocs = [
|
||||
['iran', '阿巴斯港', 27.1832, 56.2666, 'Port', '伊朗', null, null],
|
||||
['iran', '德黑兰', 35.6892, 51.389, 'Capital', '伊朗', null, null],
|
||||
['iran', '布什尔', 28.9681, 50.838, 'Base', '伊朗', null, null],
|
||||
]
|
||||
iranLocs.forEach((r) => insertLoc.run(...r))
|
||||
|
||||
db.exec(`
|
||||
INSERT OR REPLACE INTO combat_losses (side, bases_destroyed, bases_damaged, personnel_killed, personnel_wounded, aircraft, warships, armor, vehicles) VALUES
|
||||
('us', 0, 27, 127, 384, 2, 0, 0, 8),
|
||||
('iran', 3, 8, 2847, 5620, 24, 12, 18, 42);
|
||||
`)
|
||||
|
||||
db.exec('DELETE FROM wall_street_trend')
|
||||
const trendRows = [['2025-03-01T00:00:00', 82], ['2025-03-01T03:00:00', 85], ['2025-03-01T06:00:00', 88], ['2025-03-01T09:00:00', 90], ['2025-03-01T12:00:00', 92], ['2025-03-01T15:00:00', 94], ['2025-03-01T18:00:00', 95], ['2025-03-01T21:00:00', 96], ['2025-03-01T23:00:00', 98]]
|
||||
const insertTrend = db.prepare('INSERT INTO wall_street_trend (time, value) VALUES (?, ?)')
|
||||
trendRows.forEach(([t, v]) => insertTrend.run(t, v))
|
||||
|
||||
db.exec('INSERT OR REPLACE INTO retaliation_current (id, value) VALUES (1, 78)')
|
||||
db.exec('DELETE FROM retaliation_history')
|
||||
const retRows = [['2025-03-01T00:00:00', 42], ['2025-03-01T03:00:00', 48], ['2025-03-01T06:00:00', 55], ['2025-03-01T09:00:00', 61], ['2025-03-01T12:00:00', 58], ['2025-03-01T15:00:00', 65], ['2025-03-01T18:00:00', 72], ['2025-03-01T21:00:00', 76], ['2025-03-01T23:00:00', 78]]
|
||||
const insertRet = db.prepare('INSERT INTO retaliation_history (time, value) VALUES (?, ?)')
|
||||
retRows.forEach(([t, v]) => insertRet.run(t, v))
|
||||
|
||||
db.exec('DELETE FROM situation_update')
|
||||
const updateRows = [
|
||||
['u1', new Date(Date.now() - 3600000).toISOString(), 'deployment', '美军航母打击群在阿拉伯海重新部署', 'medium'],
|
||||
['u2', new Date(Date.now() - 7200000).toISOString(), 'alert', '霍尔木兹海峡海军巡逻活动加强', 'high'],
|
||||
['u3', new Date(Date.now() - 10800000).toISOString(), 'intel', '卫星图像显示阿巴斯港活动增加', 'low'],
|
||||
['u4', new Date(Date.now() - 14400000).toISOString(), 'diplomatic', '阿曼间接谈判进行中', 'low'],
|
||||
]
|
||||
const insertUpdate = db.prepare('INSERT INTO situation_update (id, timestamp, category, summary, severity) VALUES (?, ?, ?, ?, ?)')
|
||||
updateRows.forEach((row) => insertUpdate.run(...row))
|
||||
|
||||
db.prepare("INSERT OR REPLACE INTO situation (id, data, updated_at) VALUES (1, '{}', ?)").run('2026-03-01T11:45:00.000Z')
|
||||
console.log('Seed completed.')
|
||||
}
|
||||
|
||||
seed()
|
||||
108
server/situationData.js
Normal file
108
server/situationData.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const db = require('./db')
|
||||
|
||||
function toAsset(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
count: row.count,
|
||||
status: row.status,
|
||||
...(row.lat != null && { location: { lat: row.lat, lng: row.lng } }),
|
||||
}
|
||||
}
|
||||
|
||||
function toLosses(row) {
|
||||
return {
|
||||
bases: { destroyed: row.bases_destroyed, damaged: row.bases_damaged },
|
||||
personnelCasualties: { killed: row.personnel_killed, wounded: row.personnel_wounded },
|
||||
aircraft: row.aircraft,
|
||||
warships: row.warships,
|
||||
armor: row.armor,
|
||||
vehicles: row.vehicles,
|
||||
}
|
||||
}
|
||||
|
||||
const defaultLosses = {
|
||||
bases: { destroyed: 0, damaged: 0 },
|
||||
personnelCasualties: { killed: 0, wounded: 0 },
|
||||
aircraft: 0,
|
||||
warships: 0,
|
||||
armor: 0,
|
||||
vehicles: 0,
|
||||
}
|
||||
|
||||
function getSituation() {
|
||||
const summaryUs = db.prepare('SELECT * FROM force_summary WHERE side = ?').get('us')
|
||||
const summaryIr = db.prepare('SELECT * FROM force_summary WHERE side = ?').get('iran')
|
||||
const powerUs = db.prepare('SELECT * FROM power_index WHERE side = ?').get('us')
|
||||
const powerIr = db.prepare('SELECT * FROM power_index WHERE side = ?').get('iran')
|
||||
const assetsUs = db.prepare('SELECT * FROM force_asset WHERE side = ? ORDER BY id').all('us')
|
||||
const assetsIr = db.prepare('SELECT * FROM force_asset WHERE side = ? ORDER BY id').all('iran')
|
||||
const locUs = db.prepare('SELECT id, name, lat, lng, type, region, status, damage_level FROM key_location WHERE side = ?').all('us')
|
||||
const locIr = db.prepare('SELECT id, name, lat, lng, type, region FROM key_location WHERE side = ?').all('iran')
|
||||
const lossesUs = db.prepare('SELECT * FROM combat_losses WHERE side = ?').get('us')
|
||||
const lossesIr = db.prepare('SELECT * FROM combat_losses WHERE side = ?').get('iran')
|
||||
const trend = db.prepare('SELECT time, value FROM wall_street_trend ORDER BY time').all()
|
||||
const retaliationCur = db.prepare('SELECT value FROM retaliation_current WHERE id = 1').get()
|
||||
const retaliationHist = db.prepare('SELECT time, value FROM retaliation_history ORDER BY time').all()
|
||||
const updates = db.prepare('SELECT * FROM situation_update ORDER BY timestamp DESC').all()
|
||||
const meta = db.prepare('SELECT updated_at FROM situation WHERE id = 1').get()
|
||||
|
||||
return {
|
||||
lastUpdated: meta?.updated_at || new Date().toISOString(),
|
||||
usForces: {
|
||||
summary: {
|
||||
totalAssets: summaryUs?.total_assets ?? 0,
|
||||
personnel: summaryUs?.personnel ?? 0,
|
||||
navalShips: summaryUs?.naval_ships ?? 0,
|
||||
aircraft: summaryUs?.aircraft ?? 0,
|
||||
groundUnits: summaryUs?.ground_units ?? 0,
|
||||
uav: summaryUs?.uav ?? 0,
|
||||
missileConsumed: summaryUs?.missile_consumed ?? 0,
|
||||
missileStock: summaryUs?.missile_stock ?? 0,
|
||||
},
|
||||
powerIndex: {
|
||||
overall: powerUs?.overall ?? 0,
|
||||
militaryStrength: powerUs?.military_strength ?? 0,
|
||||
economicPower: powerUs?.economic_power ?? 0,
|
||||
geopoliticalInfluence: powerUs?.geopolitical_influence ?? 0,
|
||||
},
|
||||
assets: (assetsUs || []).map(toAsset),
|
||||
keyLocations: locUs || [],
|
||||
combatLosses: lossesUs ? toLosses(lossesUs) : defaultLosses,
|
||||
wallStreetInvestmentTrend: trend || [],
|
||||
},
|
||||
iranForces: {
|
||||
summary: {
|
||||
totalAssets: summaryIr?.total_assets ?? 0,
|
||||
personnel: summaryIr?.personnel ?? 0,
|
||||
navalShips: summaryIr?.naval_ships ?? 0,
|
||||
aircraft: summaryIr?.aircraft ?? 0,
|
||||
groundUnits: summaryIr?.ground_units ?? 0,
|
||||
uav: summaryIr?.uav ?? 0,
|
||||
missileConsumed: summaryIr?.missile_consumed ?? 0,
|
||||
missileStock: summaryIr?.missile_stock ?? 0,
|
||||
},
|
||||
powerIndex: {
|
||||
overall: powerIr?.overall ?? 0,
|
||||
militaryStrength: powerIr?.military_strength ?? 0,
|
||||
economicPower: powerIr?.economic_power ?? 0,
|
||||
geopoliticalInfluence: powerIr?.geopolitical_influence ?? 0,
|
||||
},
|
||||
assets: (assetsIr || []).map(toAsset),
|
||||
keyLocations: locIr || [],
|
||||
combatLosses: lossesIr ? toLosses(lossesIr) : defaultLosses,
|
||||
retaliationSentiment: retaliationCur?.value ?? 0,
|
||||
retaliationSentimentHistory: retaliationHist || [],
|
||||
},
|
||||
recentUpdates: (updates || []).map((u) => ({
|
||||
id: u.id,
|
||||
timestamp: u.timestamp,
|
||||
category: u.category,
|
||||
summary: u.summary,
|
||||
severity: u.severity,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getSituation }
|
||||
7
src/api/situation.ts
Normal file
7
src/api/situation.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { MilitarySituation } from '@/data/mockData'
|
||||
|
||||
export async function fetchSituation(): Promise<MilitarySituation> {
|
||||
const res = await fetch('/api/situation')
|
||||
if (!res.ok) throw new Error(`API error: ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
30
src/api/websocket.ts
Normal file
30
src/api/websocket.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
type Handler = (data: unknown) => void
|
||||
|
||||
let ws: WebSocket | null = null
|
||||
let handler: Handler | null = null
|
||||
|
||||
function getUrl(): string {
|
||||
return `${window.location.origin.replace(/^http/, 'ws')}/ws`
|
||||
}
|
||||
|
||||
export function connectSituationWebSocket(onData: Handler): () => void {
|
||||
handler = onData
|
||||
if (ws?.readyState === WebSocket.OPEN) return () => {}
|
||||
|
||||
ws = new WebSocket(getUrl())
|
||||
ws.onmessage = (e) => {
|
||||
try {
|
||||
const msg = JSON.parse(e.data)
|
||||
if (msg.type === 'situation' && msg.data) handler?.(msg.data)
|
||||
} catch (_) {}
|
||||
}
|
||||
ws.onclose = () => {
|
||||
ws = null
|
||||
setTimeout(() => handler && connectSituationWebSocket(handler), 3000)
|
||||
}
|
||||
return () => {
|
||||
handler = null
|
||||
ws?.close()
|
||||
ws = null
|
||||
}
|
||||
}
|
||||
68
src/components/BaseStatusPanel.tsx
Normal file
68
src/components/BaseStatusPanel.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useMemo } from 'react'
|
||||
import { MapPin, AlertTriangle, AlertCircle } from 'lucide-react'
|
||||
import type { MilitarySituation } from '@/data/mockData'
|
||||
|
||||
interface BaseStatusPanelProps {
|
||||
keyLocations: MilitarySituation['usForces']['keyLocations']
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TOTAL_BASES = 62
|
||||
|
||||
export function BaseStatusPanel({ keyLocations, className = '' }: BaseStatusPanelProps) {
|
||||
const stats = useMemo(() => {
|
||||
const bases = (keyLocations || []).filter((loc) => loc.type === 'Base')
|
||||
let attacked = 0
|
||||
let severe = 0
|
||||
let moderate = 0
|
||||
let light = 0
|
||||
for (const b of bases) {
|
||||
const s = b.status ?? 'operational'
|
||||
if (s === 'attacked') attacked++
|
||||
const lvl = b.damage_level
|
||||
if (lvl === 3) severe++
|
||||
else if (lvl === 2) moderate++
|
||||
else if (lvl === 1) light++
|
||||
}
|
||||
return { attacked, severe, moderate, light }
|
||||
}, [keyLocations])
|
||||
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
<MapPin className="h-3 w-3 shrink-0" />
|
||||
美军基地态势
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 text-xs tabular-nums">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-military-text-secondary">总基地数</span>
|
||||
<strong>{TOTAL_BASES}</strong>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-1 text-military-text-secondary">
|
||||
<AlertCircle className="h-3 w-3 text-red-400" />
|
||||
被袭击
|
||||
</span>
|
||||
<strong className="text-red-400">{stats.attacked}</strong>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-1 text-military-text-secondary">
|
||||
<AlertTriangle className="h-3 w-3 text-amber-500" />
|
||||
严重损毁
|
||||
</span>
|
||||
<strong className="text-amber-500">{stats.severe}</strong>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-military-text-secondary">中度损毁</span>
|
||||
<strong className="text-amber-400">{stats.moderate}</strong>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-military-text-secondary">轻度损毁</span>
|
||||
<strong className="text-amber-300">{stats.light}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -24,15 +24,34 @@ export function HeaderPanel() {
|
||||
second: '2-digit',
|
||||
})
|
||||
|
||||
const formatDataTime = (iso: string) => {
|
||||
const d = new Date(iso)
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex shrink-0 flex-wrap items-center justify-between gap-3 overflow-x-auto border-b border-military-border bg-military-panel/95 px-4 py-3 font-orbitron lg:flex-nowrap lg:px-6">
|
||||
<div className="flex flex-wrap items-center gap-3 lg:gap-6">
|
||||
<h1 className="text-base font-bold uppercase tracking-widest text-military-accent lg:text-2xl">
|
||||
美伊军事态势显示
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-military-text-secondary">
|
||||
<Clock className="h-4 w-4 shrink-0" />
|
||||
<span className="min-w-[11rem] tabular-nums">{formatDateTime(now)}</span>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-2 text-sm text-military-text-secondary">
|
||||
<Clock className="h-4 w-4 shrink-0" />
|
||||
<span className="min-w-[11rem] tabular-nums">{formatDateTime(now)}</span>
|
||||
</div>
|
||||
{isConnected && (
|
||||
<span className="text-[10px] text-green-500/90">
|
||||
{formatDataTime(situation.lastUpdated)} (实时更新)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
|
||||
@@ -1,51 +1,213 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import Map, { Marker } from 'react-map-gl'
|
||||
import { useMemo, useEffect, useRef } from 'react'
|
||||
import Map, { Source, Layer } from 'react-map-gl'
|
||||
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'
|
||||
|
||||
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN || ''
|
||||
|
||||
// Persian Gulf center
|
||||
const DEFAULT_VIEW = {
|
||||
longitude: 54,
|
||||
latitude: 27,
|
||||
zoom: 5.5,
|
||||
const DEFAULT_VIEW = { longitude: 52.5, latitude: 26.5, zoom: 5.2 }
|
||||
|
||||
const COUNTRIES_GEOJSON =
|
||||
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson'
|
||||
|
||||
const IRAN_ADMIN = 'Iran'
|
||||
const ALLIES_ADMIN = [
|
||||
'Qatar',
|
||||
'Bahrain',
|
||||
'Kuwait',
|
||||
'United Arab Emirates',
|
||||
'Saudi Arabia',
|
||||
'Iraq',
|
||||
'Syria',
|
||||
'Jordan',
|
||||
'Turkey',
|
||||
'Israel',
|
||||
'Oman',
|
||||
'Egypt',
|
||||
'Djibouti',
|
||||
]
|
||||
|
||||
// 伊朗攻击源 德黑兰 [lng, lat]
|
||||
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
|
||||
|
||||
function parabolaPath(
|
||||
start: [number, number],
|
||||
end: [number, number],
|
||||
height = 2
|
||||
): [number, number][] {
|
||||
const midLng = (start[0] + end[0]) / 2
|
||||
const midLat = (start[1] + end[1]) / 2 + height
|
||||
return [start, [midLng, midLat], end]
|
||||
}
|
||||
|
||||
type BaseStatus = 'operational' | 'damaged' | 'attacked'
|
||||
|
||||
interface KeyLoc {
|
||||
name: string
|
||||
lat: number
|
||||
lng: number
|
||||
type?: string
|
||||
status?: BaseStatus
|
||||
damage_level?: number
|
||||
}
|
||||
|
||||
function toFeature(loc: KeyLoc, side: 'us' | 'iran', status?: BaseStatus) {
|
||||
return {
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
side,
|
||||
name: loc.name,
|
||||
status: status ?? (loc as KeyLoc & { status?: BaseStatus }).status ?? 'operational',
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point' as const,
|
||||
coordinates: [loc.lng, loc.lat] as [number, number],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function WarMap() {
|
||||
const mapRef = useRef<MapRef>(null)
|
||||
const animRef = useRef<number>(0)
|
||||
const startRef = useRef<number>(0)
|
||||
const { situation } = useSituationStore()
|
||||
const { usForces, iranForces } = situation
|
||||
|
||||
const usMarkers = usForces.keyLocations
|
||||
const iranMarkers = iranForces.keyLocations
|
||||
const usLocs = (usForces.keyLocations || []) as KeyLoc[]
|
||||
const irLocs = (iranForces.keyLocations || []) as KeyLoc[]
|
||||
|
||||
const { usNaval, usBaseOp, usBaseDamaged, usBaseAttacked, labelsGeoJson } = useMemo(() => {
|
||||
const naval: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||||
const op: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||||
const damaged: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||||
const attacked: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||||
const labels: GeoJSON.Feature<GeoJSON.Point>[] = []
|
||||
|
||||
for (const loc of usLocs as KeyLoc[]) {
|
||||
const f = toFeature(loc, 'us')
|
||||
labels.push({ ...f, properties: { ...f.properties, name: loc.name } })
|
||||
if (loc.type === 'Base') {
|
||||
const s = (loc.status ?? 'operational') as BaseStatus
|
||||
if (s === 'attacked') attacked.push(f)
|
||||
else if (s === 'damaged') damaged.push(f)
|
||||
else op.push(f)
|
||||
} else {
|
||||
naval.push(f)
|
||||
}
|
||||
}
|
||||
|
||||
for (const loc of irLocs) {
|
||||
const f = toFeature(loc, 'iran')
|
||||
labels.push({ ...f, properties: { ...f.properties, name: loc.name } })
|
||||
}
|
||||
|
||||
return {
|
||||
usNaval: { type: 'FeatureCollection' as const, features: naval },
|
||||
usBaseOp: { type: 'FeatureCollection' as const, features: op },
|
||||
usBaseDamaged: { type: 'FeatureCollection' as const, features: damaged },
|
||||
usBaseAttacked: { type: 'FeatureCollection' as const, features: attacked },
|
||||
labelsGeoJson: { type: 'FeatureCollection' as const, features: labels },
|
||||
}
|
||||
}, [usForces.keyLocations, iranForces.keyLocations])
|
||||
|
||||
// 德黑兰到 27 个被袭目标的攻击曲线
|
||||
const attackLinesGeoJson = useMemo(
|
||||
() => ({
|
||||
type: 'FeatureCollection' as const,
|
||||
features: ATTACKED_TARGETS.map((target) => ({
|
||||
type: 'Feature' as const,
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'LineString' as const,
|
||||
coordinates: parabolaPath(TEHRAN_SOURCE, target as [number, number]),
|
||||
},
|
||||
})),
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const hideNonBelligerentLabels = (map: MapboxMap) => {
|
||||
const labelLayers = [
|
||||
'country-label',
|
||||
'state-label',
|
||||
'place-label',
|
||||
'place-label-capital',
|
||||
'place-label-city',
|
||||
'place-label-town',
|
||||
'place-label-village',
|
||||
'poi-label',
|
||||
]
|
||||
for (const id of labelLayers) {
|
||||
try {
|
||||
if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', 'none')
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current?.getMap()
|
||||
if (!map) return
|
||||
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)
|
||||
}
|
||||
// damaged: 橙色闪烁 opacity 0.5 ~ 1, 约 1s 周期
|
||||
if (map.getLayer('points-damaged')) {
|
||||
const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.003)
|
||||
map.setPaintProperty('points-damaged', 'circle-opacity', blink)
|
||||
}
|
||||
// attacked: 红色脉冲 2s 循环, 扩散半径 0→40px, opacity 1→0 (map.md)
|
||||
if (map.getLayer('points-attacked-pulse')) {
|
||||
const cycle = 2000
|
||||
const phase = (elapsed % cycle) / cycle
|
||||
const r = 40 * phase
|
||||
const opacity = 1 - phase
|
||||
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
|
||||
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
|
||||
}
|
||||
} catch (_) {}
|
||||
animRef.current = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
hideNonBelligerentLabels(map)
|
||||
if (map.getLayer('attack-lines')) {
|
||||
animRef.current = requestAnimationFrame(tick)
|
||||
} else {
|
||||
animRef.current = requestAnimationFrame(start)
|
||||
}
|
||||
}
|
||||
if (map.isStyleLoaded()) start()
|
||||
else map.once('load', start)
|
||||
|
||||
return () => cancelAnimationFrame(animRef.current)
|
||||
}, [])
|
||||
|
||||
// Fallback when no Mapbox token - show placeholder
|
||||
if (!MAPBOX_TOKEN) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-military-dark">
|
||||
<div className="rounded-lg border border-military-border bg-military-panel p-8 text-center">
|
||||
<p className="font-orbitron text-sm text-military-text-primary">
|
||||
地图需要 Mapbox 令牌
|
||||
</p>
|
||||
<div className="rounded-lg border border-military-border bg-military-panel/95 p-8 text-center shadow-lg">
|
||||
<p className="font-orbitron text-sm font-medium text-military-text-primary">地图需要 Mapbox 令牌</p>
|
||||
<p className="mt-2 text-xs text-military-text-secondary">
|
||||
请在 .env 中设置 VITE_MAPBOX_ACCESS_TOKEN
|
||||
复制 .env.example 为 .env,填入令牌后重启
|
||||
</p>
|
||||
<div className="mt-4 flex justify-center gap-8">
|
||||
<div>
|
||||
<p className="text-[10px] text-military-us">美方位置</p>
|
||||
{usMarkers.map((loc) => (
|
||||
<p key={loc.name} className="text-xs text-military-text-primary">{loc.name}</p>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-military-iran">伊方位置</p>
|
||||
{iranMarkers.map((loc) => (
|
||||
<p key={loc.name} className="text-xs text-military-text-primary">{loc.name}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="https://account.mapbox.com/access-tokens/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-1 inline-block text-[10px] text-military-accent hover:underline"
|
||||
>
|
||||
免费申请 Mapbox 令牌 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -59,40 +221,160 @@ export function WarMap() {
|
||||
mapStyle="mapbox://styles/mapbox/dark-v11"
|
||||
mapboxAccessToken={MAPBOX_TOKEN}
|
||||
attributionControl={false}
|
||||
dragRotate={false}
|
||||
touchRotate={false}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
{usMarkers.map((loc) => (
|
||||
<Marker
|
||||
key={`us-${loc.name}`}
|
||||
longitude={loc.lng}
|
||||
latitude={loc.lat}
|
||||
anchor="bottom"
|
||||
color="#3B82F6"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="h-4 w-4 rounded-full border-2 border-white bg-military-us shadow-lg" />
|
||||
<span className="mt-1 rounded bg-military-panel px-1.5 py-0.5 font-orbitron text-[10px] text-military-us">
|
||||
{loc.name}
|
||||
</span>
|
||||
</div>
|
||||
</Marker>
|
||||
))}
|
||||
{iranMarkers.map((loc) => (
|
||||
<Marker
|
||||
key={`ir-${loc.name}`}
|
||||
longitude={loc.lng}
|
||||
latitude={loc.lat}
|
||||
anchor="bottom"
|
||||
color="#EF4444"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="h-4 w-4 rounded-full border-2 border-white bg-military-iran shadow-lg" />
|
||||
<span className="mt-1 rounded bg-military-panel px-1.5 py-0.5 font-orbitron text-[10px] text-military-iran">
|
||||
{loc.name}
|
||||
</span>
|
||||
</div>
|
||||
</Marker>
|
||||
))}
|
||||
{/* 美国海军 - 蓝色 */}
|
||||
<Source id="points-us-naval" type="geojson" data={usNaval}>
|
||||
<Layer
|
||||
id="points-us-naval"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#3B82F6',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#fff',
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
{/* 美军基地-正常 - 绿色 */}
|
||||
<Source id="points-us-base-op" type="geojson" data={usBaseOp}>
|
||||
<Layer
|
||||
id="points-us-base-op"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#22C55E',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#fff',
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
{/* 美军基地-损毁 - 橙色闪烁 */}
|
||||
<Source id="points-us-base-damaged" type="geojson" data={usBaseDamaged}>
|
||||
<Layer
|
||||
id="points-damaged"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#F97316',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#fff',
|
||||
'circle-opacity': 1,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
{/* 美军基地-遭袭 - 红点 + 脉冲 */}
|
||||
<Source id="points-us-base-attacked" type="geojson" data={usBaseAttacked}>
|
||||
<Layer
|
||||
id="points-attacked-dot"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#EF4444',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#fff',
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="points-attacked-pulse"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#EF4444',
|
||||
'circle-opacity': 0,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
{/* 伊朗 - 红色 */}
|
||||
<Source
|
||||
id="points-iran"
|
||||
type="geojson"
|
||||
data={{
|
||||
type: 'FeatureCollection',
|
||||
features: irLocs.map((loc) => toFeature(loc, 'iran')),
|
||||
}}
|
||||
>
|
||||
<Layer
|
||||
id="points-iran"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#EF4444',
|
||||
'circle-stroke-width': 2,
|
||||
'circle-stroke-color': '#fff',
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
<Source id="attack-lines" type="geojson" data={attackLinesGeoJson}>
|
||||
<Layer
|
||||
id="attack-lines"
|
||||
type="line"
|
||||
paint={{
|
||||
'line-color': '#ff0000',
|
||||
'line-width': 2,
|
||||
'line-dasharray': [2, 2],
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
{/* 中文标注 */}
|
||||
<Source id="labels" type="geojson" data={labelsGeoJson}>
|
||||
<Layer
|
||||
id="labels"
|
||||
type="symbol"
|
||||
layout={{
|
||||
'text-field': ['get', 'name'],
|
||||
'text-size': 11,
|
||||
'text-anchor': 'top',
|
||||
'text-offset': [0, 0.8],
|
||||
}}
|
||||
paint={{
|
||||
'text-color': '#E5E7EB',
|
||||
'text-halo-color': 'rgba(0,0,0,0.8)',
|
||||
'text-halo-width': 2,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
<Source id="countries" type="geojson" data={COUNTRIES_GEOJSON}>
|
||||
{/* 以色列 mesh 高亮 */}
|
||||
<Layer
|
||||
id="israel-fill"
|
||||
type="fill"
|
||||
filter={['==', ['get', 'ADMIN'], 'Israel']}
|
||||
paint={{
|
||||
'fill-color': 'rgba(96, 165, 250, 0.25)',
|
||||
'fill-outline-color': '#60A5FA',
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="iran-outline"
|
||||
type="line"
|
||||
filter={['==', ['get', 'ADMIN'], IRAN_ADMIN]}
|
||||
paint={{
|
||||
'line-color': '#EF4444',
|
||||
'line-width': 2,
|
||||
'line-opacity': 0.9,
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="allies-outline"
|
||||
type="line"
|
||||
filter={['in', ['get', 'ADMIN'], ['literal', ALLIES_ADMIN]]}
|
||||
paint={{
|
||||
'line-color': '#3B82F6',
|
||||
'line-width': 1.5,
|
||||
'line-opacity': 0.8,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
</Map>
|
||||
</div>
|
||||
)
|
||||
|
||||
140
src/data/mapLocations.ts
Normal file
140
src/data/mapLocations.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/** 航母标记 - 全部中文 */
|
||||
export const CARRIER_MARKERS = [
|
||||
{
|
||||
id: 'CVN-72',
|
||||
name: '林肯号航母',
|
||||
coordinates: [58.4215, 24.1568] as [number, number],
|
||||
type: 'Aircraft Carrier',
|
||||
status: 'Active - Combat Readiness',
|
||||
details: '林肯号航母打击群 (CSG-3) 部署于北阿拉伯海。',
|
||||
},
|
||||
{
|
||||
id: 'CVN-78',
|
||||
name: '福特号航母',
|
||||
coordinates: [24.1002, 35.7397] as [number, number],
|
||||
type: 'Aircraft Carrier',
|
||||
status: 'Active - Forward Deployed',
|
||||
details: '距克里特苏达湾约 15 公里。',
|
||||
},
|
||||
]
|
||||
|
||||
export type KeyLocItem = {
|
||||
name: string
|
||||
lat: number
|
||||
lng: number
|
||||
type?: string
|
||||
region?: string
|
||||
status?: 'operational' | 'damaged' | 'attacked'
|
||||
damage_level?: number
|
||||
}
|
||||
|
||||
/** 美军基地总数 62,被袭击 27 个。损毁程度:严重 6 / 中度 12 / 轻度 9 */
|
||||
const ATTACKED_BASES = [
|
||||
// 严重损毁 (6): 高价值目标,近伊朗
|
||||
{ name: '阿萨德空军基地', lat: 33.785, lng: 42.441, region: '伊拉克', damage_level: 3 },
|
||||
{ name: '巴格达外交支援中心', lat: 33.315, lng: 44.366, region: '伊拉克', damage_level: 3 },
|
||||
{ name: '乌代德空军基地', lat: 25.117, lng: 51.314, region: '卡塔尔', damage_level: 3 },
|
||||
{ name: '埃尔比勒空军基地', lat: 36.237, lng: 43.963, region: '伊拉克', damage_level: 3 },
|
||||
{ name: '因吉尔利克空军基地', lat: 37.002, lng: 35.425, region: '土耳其', damage_level: 3 },
|
||||
{ name: '苏尔坦亲王空军基地', lat: 24.062, lng: 47.58, region: '沙特', damage_level: 3 },
|
||||
// 中度损毁 (12)
|
||||
{ name: '塔吉军营', lat: 33.556, lng: 44.256, region: '伊拉克', damage_level: 2 },
|
||||
{ name: '阿因·阿萨德', lat: 33.8, lng: 42.45, region: '伊拉克', damage_level: 2 },
|
||||
{ name: '坦夫驻军', lat: 33.49, lng: 38.618, region: '叙利亚', damage_level: 2 },
|
||||
{ name: '沙达迪基地', lat: 36.058, lng: 40.73, region: '叙利亚', damage_level: 2 },
|
||||
{ name: '康诺克气田基地', lat: 35.336, lng: 40.295, region: '叙利亚', damage_level: 2 },
|
||||
{ name: '尔梅兰着陆区', lat: 37.015, lng: 41.885, region: '叙利亚', damage_level: 2 },
|
||||
{ name: '阿里夫坚军营', lat: 28.832, lng: 47.799, region: '科威特', damage_level: 2 },
|
||||
{ name: '阿里·萨勒姆空军基地', lat: 29.346, lng: 47.52, region: '科威特', damage_level: 2 },
|
||||
{ name: '巴林海军支援站', lat: 26.236, lng: 50.608, region: '巴林', damage_level: 2 },
|
||||
{ name: '达夫拉空军基地', lat: 24.248, lng: 54.547, region: '阿联酋', damage_level: 2 },
|
||||
{ name: '埃斯康村', lat: 24.774, lng: 46.738, region: '沙特', damage_level: 2 },
|
||||
{ name: '内瓦提姆空军基地', lat: 31.208, lng: 35.012, region: '以色列', damage_level: 2 },
|
||||
// 轻度损毁 (9)
|
||||
{ name: '布林军营', lat: 29.603, lng: 47.456, region: '科威特', damage_level: 1 },
|
||||
{ name: '赛利耶军营', lat: 25.275, lng: 51.52, region: '卡塔尔', damage_level: 1 },
|
||||
{ name: '拉蒙空军基地', lat: 30.776, lng: 34.666, region: '以色列', damage_level: 1 },
|
||||
{ name: '穆瓦法克·萨尔蒂空军基地', lat: 32.356, lng: 36.259, region: '约旦', damage_level: 1 },
|
||||
{ name: '屈雷吉克雷达站', lat: 38.354, lng: 37.794, region: '土耳其', damage_level: 1 },
|
||||
{ name: '苏姆莱特空军基地', lat: 17.666, lng: 54.024, region: '阿曼', damage_level: 1 },
|
||||
{ name: '马西拉空军基地', lat: 20.675, lng: 58.89, region: '阿曼', damage_level: 1 },
|
||||
{ name: '西开罗空军基地', lat: 30.915, lng: 30.298, region: '埃及', damage_level: 1 },
|
||||
{ name: '勒莫尼耶军营', lat: 11.547, lng: 43.159, region: '吉布提', damage_level: 1 },
|
||||
]
|
||||
|
||||
/** 35 个新增 operational 基地 */
|
||||
const NEW_BASES: KeyLocItem[] = [
|
||||
{ name: '多哈后勤中心', lat: 25.29, lng: 51.53, type: 'Base', region: '卡塔尔' },
|
||||
{ name: '贾法勒海军站', lat: 26.22, lng: 50.62, type: 'Base', region: '巴林' },
|
||||
{ name: '阿兹祖尔前方作战点', lat: 29.45, lng: 47.9, type: 'Base', region: '科威特' },
|
||||
{ name: '艾哈迈迪后勤枢纽', lat: 29.08, lng: 48.09, type: 'Base', region: '科威特' },
|
||||
{ name: '富查伊拉港站', lat: 25.13, lng: 56.35, type: 'Base', region: '阿联酋' },
|
||||
{ name: '哈伊马角前方点', lat: 25.79, lng: 55.94, type: 'Base', region: '阿联酋' },
|
||||
{ name: '利雅得联络站', lat: 24.71, lng: 46.68, type: 'Base', region: '沙特' },
|
||||
{ name: '朱拜勒港支援点', lat: 27.0, lng: 49.65, type: 'Base', region: '沙特' },
|
||||
{ name: '塔布克空军前哨', lat: 28.38, lng: 36.6, type: 'Base', region: '沙特' },
|
||||
{ name: '拜莱德空军基地', lat: 33.94, lng: 44.36, type: 'Base', region: '伊拉克' },
|
||||
{ name: '巴士拉后勤站', lat: 30.5, lng: 47.78, type: 'Base', region: '伊拉克' },
|
||||
{ name: '基尔库克前哨', lat: 35.47, lng: 44.35, type: 'Base', region: '伊拉克' },
|
||||
{ name: '摩苏尔支援点', lat: 36.34, lng: 43.14, type: 'Base', region: '伊拉克' },
|
||||
{ name: '哈塞克联络站', lat: 36.5, lng: 40.75, type: 'Base', region: '叙利亚' },
|
||||
{ name: '代尔祖尔前哨', lat: 35.33, lng: 40.14, type: 'Base', region: '叙利亚' },
|
||||
{ name: '安曼协调中心', lat: 31.95, lng: 35.93, type: 'Base', region: '约旦' },
|
||||
{ name: '伊兹密尔支援站', lat: 38.42, lng: 27.14, type: 'Base', region: '土耳其' },
|
||||
{ name: '哈泽瑞姆空军基地', lat: 31.07, lng: 34.84, type: 'Base', region: '以色列' },
|
||||
{ name: '杜古姆港站', lat: 19.66, lng: 57.76, type: 'Base', region: '阿曼' },
|
||||
{ name: '塞拉莱前方点', lat: 17.01, lng: 54.1, type: 'Base', region: '阿曼' },
|
||||
{ name: '亚历山大港联络站', lat: 31.2, lng: 29.9, type: 'Base', region: '埃及' },
|
||||
{ name: '卢克索前哨', lat: 25.69, lng: 32.64, type: 'Base', region: '埃及' },
|
||||
{ name: '吉布提港支援点', lat: 11.59, lng: 43.15, type: 'Base', region: '吉布提' },
|
||||
{ name: '卡塔尔应急医疗站', lat: 25.22, lng: 51.45, type: 'Base', region: '卡塔尔' },
|
||||
{ name: '沙特哈立德国王基地', lat: 24.96, lng: 46.7, type: 'Base', region: '沙特' },
|
||||
{ name: '伊拉克巴拉德联勤站', lat: 33.75, lng: 44.25, type: 'Base', region: '伊拉克' },
|
||||
{ name: '叙利亚奥马尔油田站', lat: 36.22, lng: 40.45, type: 'Base', region: '叙利亚' },
|
||||
{ name: '约旦侯赛因国王基地', lat: 31.72, lng: 36.01, type: 'Base', region: '约旦' },
|
||||
{ name: '土耳其巴特曼站', lat: 37.88, lng: 41.13, type: 'Base', region: '土耳其' },
|
||||
{ name: '以色列帕尔马欣站', lat: 31.9, lng: 34.95, type: 'Base', region: '以色列' },
|
||||
{ name: '阿曼杜古姆扩建点', lat: 19.55, lng: 57.8, type: 'Base', region: '阿曼' },
|
||||
{ name: '埃及纳特龙湖站', lat: 30.37, lng: 30.2, type: 'Base', region: '埃及' },
|
||||
{ name: '吉布提查贝尔达站', lat: 11.73, lng: 42.9, type: 'Base', region: '吉布提' },
|
||||
{ name: '阿联酋迪拜港联络', lat: 25.27, lng: 55.3, type: 'Base', region: '阿联酋' },
|
||||
{ name: '伊拉克尼尼微前哨', lat: 36.22, lng: 43.1, type: 'Base', region: '伊拉克' },
|
||||
]
|
||||
|
||||
/** 美军全部地图点位:2 航母 + 9 海军 + 62 基地 */
|
||||
export const US_KEY_LOCATIONS: KeyLocItem[] = [
|
||||
...CARRIER_MARKERS.map((c) => ({
|
||||
name: c.name + ` (${c.id})`,
|
||||
lat: c.coordinates[1],
|
||||
lng: c.coordinates[0],
|
||||
type: 'Aircraft Carrier' as const,
|
||||
region: c.id === 'CVN-72' ? '北阿拉伯海' : '东地中海',
|
||||
status: 'operational' as const,
|
||||
damage_level: undefined as number | undefined,
|
||||
})),
|
||||
{ name: '驱逐舰(阿曼湾)', lat: 25.2, lng: 58.0, type: 'Destroyer', region: '阿曼湾', status: 'operational' },
|
||||
{ name: '海岸警卫队 1', lat: 25.4, lng: 58.2, type: 'Coast Guard', region: '阿曼湾', status: 'operational' },
|
||||
{ name: '海岸警卫队 2', lat: 25.0, lng: 57.8, type: 'Coast Guard', region: '阿曼湾', status: 'operational' },
|
||||
{ name: '驱逐舰(波斯湾北部)', lat: 26.5, lng: 51.0, type: 'Destroyer', region: '波斯湾', status: 'operational' },
|
||||
{ name: '护卫舰 1', lat: 26.7, lng: 50.6, type: 'Frigate', region: '波斯湾', status: 'operational' },
|
||||
{ name: '护卫舰 2', lat: 27.0, lng: 50.2, type: 'Frigate', region: '波斯湾', status: 'operational' },
|
||||
{ name: '护卫舰 3', lat: 26.3, lng: 50.8, type: 'Frigate', region: '波斯湾', status: 'operational' },
|
||||
{ name: '辅助舰 1', lat: 26.0, lng: 51.2, type: 'Auxiliary', region: '波斯湾', status: 'operational' },
|
||||
{ name: '辅助舰 2', lat: 25.8, lng: 51.5, type: 'Auxiliary', region: '波斯湾', status: 'operational' },
|
||||
{ name: '辅助舰 3', lat: 26.2, lng: 50.9, type: 'Auxiliary', region: '波斯湾', status: 'operational' },
|
||||
...ATTACKED_BASES.map((b) => ({
|
||||
...b,
|
||||
type: 'Base' as const,
|
||||
status: 'attacked' as const,
|
||||
})),
|
||||
...NEW_BASES,
|
||||
]
|
||||
|
||||
/** 被袭击的 27 个基地坐标 [lng, lat],用于绘制攻击曲线 */
|
||||
export const ATTACKED_TARGETS: [number, number][] = ATTACKED_BASES.map((b) => [b.lng, b.lat])
|
||||
|
||||
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: '伊朗' },
|
||||
{ name: '布什尔', lat: 28.9681, lng: 50.838, type: 'Base', region: '伊朗' },
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
// TypeScript interfaces for military situation data
|
||||
import { US_KEY_LOCATIONS, IRAN_KEY_LOCATIONS } from './mapLocations'
|
||||
|
||||
export interface ForceAsset {
|
||||
id: string
|
||||
@@ -50,7 +51,16 @@ export interface MilitarySituation {
|
||||
summary: ForceSummary
|
||||
powerIndex: PowerIndex
|
||||
assets: ForceAsset[]
|
||||
keyLocations: { name: string; lat: number; lng: number }[]
|
||||
keyLocations: {
|
||||
name: string
|
||||
lat: number
|
||||
lng: number
|
||||
type?: string
|
||||
region?: string
|
||||
id?: number
|
||||
status?: 'operational' | 'damaged' | 'attacked'
|
||||
damage_level?: number
|
||||
}[]
|
||||
combatLosses: CombatLosses
|
||||
/** 华尔街财团投入趋势 { time: ISO string, value: 0-100 } */
|
||||
wallStreetInvestmentTrend: { time: string; value: number }[]
|
||||
@@ -59,7 +69,16 @@ export interface MilitarySituation {
|
||||
summary: ForceSummary
|
||||
powerIndex: PowerIndex
|
||||
assets: ForceAsset[]
|
||||
keyLocations: { name: string; lat: number; lng: number }[]
|
||||
keyLocations: {
|
||||
name: string
|
||||
lat: number
|
||||
lng: number
|
||||
type?: string
|
||||
region?: string
|
||||
id?: number
|
||||
status?: 'operational' | 'damaged' | 'attacked'
|
||||
damage_level?: number
|
||||
}[]
|
||||
combatLosses: CombatLosses
|
||||
/** 反击情绪指标 0-100 */
|
||||
retaliationSentiment: number
|
||||
@@ -70,16 +89,16 @@ export interface MilitarySituation {
|
||||
}
|
||||
|
||||
export const INITIAL_MOCK_DATA: MilitarySituation = {
|
||||
lastUpdated: new Date().toISOString(),
|
||||
lastUpdated: '2026-03-01T11:45:00.000Z',
|
||||
usForces: {
|
||||
summary: {
|
||||
totalAssets: 1247,
|
||||
totalAssets: 1245,
|
||||
personnel: 185000,
|
||||
navalShips: 285,
|
||||
aircraft: 1850,
|
||||
navalShips: 292,
|
||||
aircraft: 1862,
|
||||
groundUnits: 18,
|
||||
uav: 420,
|
||||
missileConsumed: 156,
|
||||
uav: 418,
|
||||
missileConsumed: 1056,
|
||||
missileStock: 2840,
|
||||
},
|
||||
powerIndex: {
|
||||
@@ -89,23 +108,24 @@ export const INITIAL_MOCK_DATA: MilitarySituation = {
|
||||
geopoliticalInfluence: 97,
|
||||
},
|
||||
assets: [
|
||||
{ id: 'us-1', name: '艾森豪威尔号航母', type: '航母', count: 1, status: 'active' },
|
||||
{ id: 'us-1', name: '双航母打击群 (CVN-72 & CVN-78)', type: '航母', count: 2, status: 'active' },
|
||||
{ id: 'us-2', name: '阿利·伯克级驱逐舰', type: '驱逐舰', count: 4, status: 'active' },
|
||||
{ id: 'us-3', name: 'F/A-18 中队', type: '战机', count: 48, status: 'active' },
|
||||
{ id: 'us-4', name: 'F-35 中队', type: '战机', count: 24, status: 'standby' },
|
||||
{ id: 'us-5', name: 'B-1B 轰炸机', type: '轰炸机', count: 4, status: 'alert' },
|
||||
{ id: 'us-6', name: '爱国者防空系统', type: '防空', count: 3, status: 'active' },
|
||||
{ id: 'us-7', name: 'MQ-9 死神', type: '无人机', count: 28, status: 'active' },
|
||||
{ id: 'us-8', name: 'MQ-1C 灰鹰', type: '无人机', count: 45, status: 'active' },
|
||||
],
|
||||
keyLocations: [
|
||||
{ name: '第五舰队司令部', lat: 26.2285, lng: 50.586 },
|
||||
{ name: '乌代德空军基地', lat: 25.1173, lng: 51.3153 },
|
||||
{ name: '艾森豪威尔号航母', lat: 26.5, lng: 52.0 },
|
||||
{ id: 'us-4', name: 'F-35 中队', type: '战机', count: 48, status: 'active' },
|
||||
{ id: 'us-5', name: 'F-22 猛禽', type: '战机', count: 12, status: 'active' },
|
||||
{ id: 'us-6', name: 'B-2 幽灵', type: '轰炸机', count: 2, status: 'alert' },
|
||||
{ id: 'us-7', name: '爱国者防空系统', type: '防空', count: 3, status: 'active' },
|
||||
{ id: 'us-8', name: 'MQ-9 死神', type: '无人机', count: 28, status: 'active' },
|
||||
{ id: 'us-9', name: 'MQ-1C 灰鹰', type: '无人机', count: 45, status: 'active' },
|
||||
],
|
||||
keyLocations: US_KEY_LOCATIONS,
|
||||
combatLosses: {
|
||||
bases: { destroyed: 0, damaged: 2 },
|
||||
personnelCasualties: { killed: 127, wounded: 384 },
|
||||
aircraft: 2,
|
||||
warships: 0,
|
||||
armor: 0,
|
||||
vehicles: 8,
|
||||
},
|
||||
wallStreetInvestmentTrend: [
|
||||
{ time: '2025-03-01T00:00:00', value: 82 },
|
||||
@@ -121,14 +141,14 @@ export const INITIAL_MOCK_DATA: MilitarySituation = {
|
||||
},
|
||||
iranForces: {
|
||||
summary: {
|
||||
totalAssets: 8523,
|
||||
totalAssets: 7850,
|
||||
personnel: 2350000,
|
||||
navalShips: 4250,
|
||||
aircraft: 8200,
|
||||
groundUnits: 350,
|
||||
uav: 1850,
|
||||
missileConsumed: 3420,
|
||||
missileStock: 15600,
|
||||
uav: 750,
|
||||
missileConsumed: 3720,
|
||||
missileStock: 13800,
|
||||
},
|
||||
powerIndex: {
|
||||
overall: 42,
|
||||
@@ -141,16 +161,13 @@ export const INITIAL_MOCK_DATA: MilitarySituation = {
|
||||
{ id: 'ir-2', name: '快攻艇', type: '海军', count: 100, status: 'active' },
|
||||
{ id: 'ir-3', name: 'F-4 Phantom', type: '战机', count: 62, status: 'standby' },
|
||||
{ id: 'ir-4', name: 'F-14 Tomcat', type: '战机', count: 24, status: 'active' },
|
||||
{ id: 'ir-5', name: '弹道导弹', type: '导弹', count: 2000, status: 'alert' },
|
||||
{ id: 'ir-5', name: '弹道导弹', type: '导弹', count: 3400, status: 'alert' },
|
||||
{ id: 'ir-6', name: '伊斯兰革命卫队海军', type: '准军事', count: 25000, status: 'active' },
|
||||
{ id: 'ir-7', name: '沙希德-136', type: '无人机', count: 1200, status: 'alert' },
|
||||
{ id: 'ir-8', name: '穆哈杰-6', type: '无人机', count: 280, status: 'active' },
|
||||
],
|
||||
keyLocations: [
|
||||
{ name: '阿巴斯港', lat: 27.1832, lng: 56.2666 },
|
||||
{ name: '德黑兰', lat: 35.6892, lng: 51.3890 },
|
||||
{ name: '布什尔', lat: 28.9681, lng: 50.8380 },
|
||||
{ id: 'ir-7', name: '沙希德-136', type: '无人机', count: 750, status: 'alert' },
|
||||
{ id: 'ir-8', name: '法塔赫 (Fattah)', type: '导弹', count: 12, status: 'alert' },
|
||||
{ id: 'ir-9', name: '穆哈杰-6', type: '无人机', count: 280, status: 'active' },
|
||||
],
|
||||
keyLocations: IRAN_KEY_LOCATIONS,
|
||||
combatLosses: {
|
||||
bases: { destroyed: 3, damaged: 8 },
|
||||
personnelCasualties: { killed: 2847, wounded: 5620 },
|
||||
|
||||
@@ -3,22 +3,30 @@ import { HeaderPanel } from '@/components/HeaderPanel'
|
||||
import { ForcePanel } from '@/components/ForcePanel'
|
||||
import { WarMap } from '@/components/WarMap'
|
||||
import { CombatLossesPanel } from '@/components/CombatLossesPanel'
|
||||
import { BaseStatusPanel } from '@/components/BaseStatusPanel'
|
||||
import { PowerChart } from '@/components/PowerChart'
|
||||
import { InvestmentTrendChart } from '@/components/InvestmentTrendChart'
|
||||
import { RetaliationGauge } from '@/components/RetaliationGauge'
|
||||
import { useSituationStore } from '@/store/situationStore'
|
||||
import { startWebSocketMock, stopWebSocketMock } from '@/store/situationStore'
|
||||
import { fetchAndSetSituation, startSituationWebSocket, stopSituationWebSocket } from '@/store/situationStore'
|
||||
|
||||
export function Dashboard() {
|
||||
const situation = useSituationStore((s) => s.situation)
|
||||
const isLoading = useSituationStore((s) => s.isLoading)
|
||||
const lastError = useSituationStore((s) => s.lastError)
|
||||
|
||||
useEffect(() => {
|
||||
startWebSocketMock()
|
||||
return () => stopWebSocketMock()
|
||||
fetchAndSetSituation().finally(() => startSituationWebSocket())
|
||||
return () => stopSituationWebSocket()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full min-h-0 flex-col overflow-hidden bg-military-dark font-orbitron">
|
||||
{lastError && (
|
||||
<div className="shrink-0 bg-amber-500/20 px-4 py-2 text-center text-sm text-amber-400">
|
||||
{lastError}(使用本地缓存,请确保 API + WebSocket 已启动:npm run api)
|
||||
</div>
|
||||
)}
|
||||
<HeaderPanel />
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-auto lg:flex-row lg:overflow-hidden">
|
||||
@@ -50,10 +58,14 @@ export function Dashboard() {
|
||||
<div className="min-h-0 flex-1">
|
||||
<WarMap />
|
||||
</div>
|
||||
<CombatLossesPanel
|
||||
usLosses={situation.usForces.combatLosses}
|
||||
iranLosses={situation.iranForces.combatLosses}
|
||||
/>
|
||||
<div className="flex shrink-0 flex-col gap-2 border-t border-military-border bg-military-panel/95 px-4 py-2 lg:flex-row lg:items-stretch">
|
||||
<BaseStatusPanel keyLocations={situation.usForces.keyLocations} className="shrink-0 lg:min-w-[200px] lg:border-r lg:border-military-border lg:pr-4" />
|
||||
<CombatLossesPanel
|
||||
usLosses={situation.usForces.combatLosses}
|
||||
iranLosses={situation.iranForces.combatLosses}
|
||||
className="min-w-0 flex-1 border-t-0"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside className="flex min-h-0 min-w-0 shrink-0 flex-col gap-2 overflow-y-auto overflow-x-visible border-t border-military-border p-3 lg:min-w-[320px] lg:max-w-[340px] lg:border-t-0 lg:border-l lg:p-4">
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { create } from 'zustand'
|
||||
import type { MilitarySituation } from '@/data/mockData'
|
||||
import { INITIAL_MOCK_DATA } from '@/data/mockData'
|
||||
import { fetchSituation } from '@/api/situation'
|
||||
import { connectSituationWebSocket } from '@/api/websocket'
|
||||
|
||||
interface SituationState {
|
||||
situation: MilitarySituation
|
||||
isConnected: boolean
|
||||
lastError: string | null
|
||||
isLoading: boolean
|
||||
setSituation: (situation: MilitarySituation) => void
|
||||
updateFromWebSocket: (partial: Partial<MilitarySituation>) => void
|
||||
fetchAndSetSituation: () => Promise<void>
|
||||
setConnected: (connected: boolean) => void
|
||||
setLastError: (error: string | null) => void
|
||||
}
|
||||
@@ -16,67 +19,48 @@ export const useSituationStore = create<SituationState>((set) => ({
|
||||
situation: INITIAL_MOCK_DATA,
|
||||
isConnected: false,
|
||||
lastError: null,
|
||||
isLoading: false,
|
||||
|
||||
setSituation: (situation) => set({ situation }),
|
||||
|
||||
updateFromWebSocket: (partial) =>
|
||||
set((state) => ({
|
||||
situation: {
|
||||
...state.situation,
|
||||
...partial,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
},
|
||||
})),
|
||||
fetchAndSetSituation: async () => {
|
||||
set({ isLoading: true, lastError: null })
|
||||
try {
|
||||
const situation = await fetchSituation()
|
||||
set({ situation })
|
||||
} catch (err) {
|
||||
set({
|
||||
lastError: err instanceof Error ? err.message : 'Failed to fetch situation',
|
||||
})
|
||||
} finally {
|
||||
set({ isLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
setConnected: (isConnected) => set({ isConnected }),
|
||||
|
||||
setLastError: (lastError) => set({ lastError }),
|
||||
}))
|
||||
|
||||
// WebSocket mock logic - simulates real-time updates without actual simulation
|
||||
// In production, replace with actual WebSocket connection
|
||||
let mockWsInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
export function startWebSocketMock(): void {
|
||||
if (mockWsInterval) return
|
||||
|
||||
const store = useSituationStore.getState()
|
||||
store.setConnected(true)
|
||||
store.setLastError(null)
|
||||
|
||||
mockWsInterval = setInterval(() => {
|
||||
const { situation } = useSituationStore.getState()
|
||||
const now = Date.now()
|
||||
|
||||
// Simulate minor fluctuations in numbers (display-only, no logic)
|
||||
const fluctuation = () => Math.floor(Math.random() * 3) - 1
|
||||
|
||||
useSituationStore.getState().updateFromWebSocket({
|
||||
usForces: {
|
||||
...situation.usForces,
|
||||
summary: {
|
||||
...situation.usForces.summary,
|
||||
totalAssets: Math.max(40, situation.usForces.summary.totalAssets + fluctuation()),
|
||||
},
|
||||
},
|
||||
recentUpdates: [
|
||||
{
|
||||
id: `u-${now}`,
|
||||
timestamp: new Date(now).toISOString(),
|
||||
category: 'intel',
|
||||
summary: '例行状态同步 - 各系统正常',
|
||||
severity: 'low',
|
||||
},
|
||||
...situation.recentUpdates.slice(0, 3),
|
||||
],
|
||||
})
|
||||
}, 15000) // Update every 15 seconds
|
||||
export function fetchAndSetSituation(): Promise<void> {
|
||||
return useSituationStore.getState().fetchAndSetSituation()
|
||||
}
|
||||
|
||||
export function stopWebSocketMock(): void {
|
||||
if (mockWsInterval) {
|
||||
clearInterval(mockWsInterval)
|
||||
mockWsInterval = null
|
||||
}
|
||||
let disconnectWs: (() => void) | null = null
|
||||
|
||||
export function startSituationWebSocket(): () => void {
|
||||
useSituationStore.getState().setConnected(true)
|
||||
useSituationStore.getState().setLastError(null)
|
||||
|
||||
disconnectWs = connectSituationWebSocket((data) => {
|
||||
useSituationStore.getState().setSituation(data as MilitarySituation)
|
||||
})
|
||||
|
||||
return stopSituationWebSocket
|
||||
}
|
||||
|
||||
export function stopSituationWebSocket(): void {
|
||||
disconnectWs?.()
|
||||
disconnectWs = null
|
||||
useSituationStore.getState().setConnected(false)
|
||||
}
|
||||
|
||||
BIN
usa_logo.png
Normal file
BIN
usa_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 453 KiB |
@@ -4,6 +4,12 @@ import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': { target: 'http://localhost:3001', changeOrigin: true },
|
||||
'/ws': { target: 'ws://localhost:3001', ws: true },
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
||||
Reference in New Issue
Block a user