feat: add new file

This commit is contained in:
Daniel
2026-03-01 19:23:48 +08:00
parent d705fd6c83
commit c07fc681dd
24 changed files with 2711 additions and 166 deletions

View File

@@ -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
View File

@@ -23,6 +23,9 @@ dist-ssr
*.sln
*.sw?
# API database
server/data.db
# Env
.env
.env.local

View File

@@ -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 后生成)
```

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,5 @@
{
"name": "usa-api",
"private": true,
"type": "commonjs"
}

15
server/routes.js Normal file
View 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
View 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
View 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
View 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
View 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
}
}

View 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>
)
}

View File

@@ -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">

View File

@@ -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
View 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: '伊朗' },
]

View File

@@ -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 },

View File

@@ -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">

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

View File

@@ -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'),