diff --git a/crawler/db_merge.py b/crawler/db_merge.py index c8464c3..78fbe63 100644 --- a/crawler/db_merge.py +++ b/crawler/db_merge.py @@ -41,6 +41,11 @@ def _ensure_tables(conn: sqlite3.Connection) -> None: conn.execute("ALTER TABLE combat_losses ADD COLUMN updated_at TEXT DEFAULT (datetime('now'))") except sqlite3.OperationalError: pass + for col in ("drones", "missiles", "helicopters", "submarines"): + try: + conn.execute(f"ALTER TABLE combat_losses ADD COLUMN {col} INTEGER NOT NULL DEFAULT 0") + except sqlite3.OperationalError: + pass conn.execute("CREATE TABLE IF NOT EXISTS wall_street_trend (id INTEGER PRIMARY KEY AUTOINCREMENT, time TEXT NOT NULL, value INTEGER NOT NULL)") conn.execute("CREATE TABLE IF NOT EXISTS retaliation_current (id INTEGER PRIMARY KEY CHECK (id = 1), value INTEGER NOT NULL)") conn.execute("CREATE TABLE IF NOT EXISTS retaliation_history (id INTEGER PRIMARY KEY AUTOINCREMENT, time TEXT NOT NULL, value INTEGER NOT NULL)") @@ -74,16 +79,19 @@ def merge(extracted: Dict[str, Any], db_path: Optional[str] = None) -> bool: continue try: row = conn.execute( - "SELECT personnel_killed,personnel_wounded,civilian_killed,civilian_wounded,bases_destroyed,bases_damaged,aircraft,warships,armor,vehicles FROM combat_losses WHERE side = ?", + "SELECT personnel_killed,personnel_wounded,civilian_killed,civilian_wounded,bases_destroyed,bases_damaged,aircraft,warships,armor,vehicles,drones,missiles,helicopters,submarines FROM combat_losses WHERE side = ?", (side,), ).fetchone() cur = {"personnel_killed": 0, "personnel_wounded": 0, "civilian_killed": 0, "civilian_wounded": 0, - "bases_destroyed": 0, "bases_damaged": 0, "aircraft": 0, "warships": 0, "armor": 0, "vehicles": 0} + "bases_destroyed": 0, "bases_damaged": 0, "aircraft": 0, "warships": 0, "armor": 0, "vehicles": 0, + "drones": 0, "missiles": 0, "helicopters": 0, "submarines": 0} if row: cur = { "personnel_killed": row[0], "personnel_wounded": row[1], "civilian_killed": row[2] or 0, "civilian_wounded": row[3] or 0, "bases_destroyed": row[4], "bases_damaged": row[5], "aircraft": row[6], "warships": row[7], "armor": row[8], "vehicles": row[9], + "drones": row[10] if len(row) > 10 else 0, "missiles": row[11] if len(row) > 11 else 0, + "helicopters": row[12] if len(row) > 12 else 0, "submarines": row[13] if len(row) > 13 else 0, } pk = max(0, (cur["personnel_killed"] or 0) + delta.get("personnel_killed", 0)) pw = max(0, (cur["personnel_wounded"] or 0) + delta.get("personnel_wounded", 0)) @@ -95,18 +103,23 @@ def merge(extracted: Dict[str, Any], db_path: Optional[str] = None) -> bool: ws = max(0, (cur["warships"] or 0) + delta.get("warships", 0)) ar = max(0, (cur["armor"] or 0) + delta.get("armor", 0)) vh = max(0, (cur["vehicles"] or 0) + delta.get("vehicles", 0)) + dr = max(0, (cur["drones"] or 0) + delta.get("drones", 0)) + ms = max(0, (cur["missiles"] or 0) + delta.get("missiles", 0)) + hp = max(0, (cur["helicopters"] or 0) + delta.get("helicopters", 0)) + sb = max(0, (cur["submarines"] or 0) + delta.get("submarines", 0)) ts = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z") if row: conn.execute( """UPDATE combat_losses SET personnel_killed=?, personnel_wounded=?, civilian_killed=?, civilian_wounded=?, - bases_destroyed=?, bases_damaged=?, aircraft=?, warships=?, armor=?, vehicles=?, updated_at=? WHERE side=?""", - (pk, pw, ck, cw, bd, bm, ac, ws, ar, vh, ts, side), + bases_destroyed=?, bases_damaged=?, aircraft=?, warships=?, armor=?, vehicles=?, + drones=?, missiles=?, helicopters=?, submarines=?, updated_at=? WHERE side=?""", + (pk, pw, ck, cw, bd, bm, ac, ws, ar, vh, dr, ms, hp, sb, ts, side), ) else: conn.execute( """INSERT OR REPLACE INTO combat_losses (side, personnel_killed, personnel_wounded, civilian_killed, civilian_wounded, - bases_destroyed, bases_damaged, aircraft, warships, armor, vehicles, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", - (side, pk, pw, ck, cw, bd, bm, ac, ws, ar, vh, ts), + bases_destroyed, bases_damaged, aircraft, warships, armor, vehicles, drones, missiles, helicopters, submarines, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (side, pk, pw, ck, cw, bd, bm, ac, ws, ar, vh, dr, ms, hp, sb, ts), ) if conn.total_changes > 0: updated = True diff --git a/crawler/panel_schema.py b/crawler/panel_schema.py index 1f2029e..8dd1bcf 100644 --- a/crawler/panel_schema.py +++ b/crawler/panel_schema.py @@ -19,7 +19,7 @@ TimeSeriesPoint = Tuple[str, int] # (ISO time, value) # AI 可从新闻中提取的字段 EXTRACTABLE_FIELDS = { "situation_update": ["summary", "category", "severity", "timestamp"], - "combat_losses": ["personnel_killed", "personnel_wounded", "civilian_killed", "civilian_wounded", "bases_destroyed", "bases_damaged", "aircraft", "warships", "armor", "vehicles"], + "combat_losses": ["personnel_killed", "personnel_wounded", "civilian_killed", "civilian_wounded", "bases_destroyed", "bases_damaged", "aircraft", "warships", "armor", "vehicles", "drones", "missiles", "helicopters", "submarines"], "retaliation": ["value"], # 0-100 "wall_street_trend": ["time", "value"], # 0-100 "conflict_stats": ["estimated_casualties", "estimated_strike_count"], diff --git a/server/data.db b/server/data.db index 0ecd106..0754b25 100644 Binary files a/server/data.db and b/server/data.db differ diff --git a/server/data.db-shm b/server/data.db-shm index 5c447fe..39aaafb 100644 Binary files a/server/data.db-shm and b/server/data.db-shm differ diff --git a/server/data.db-wal b/server/data.db-wal index 268c306..4acc4df 100644 Binary files a/server/data.db-wal and b/server/data.db-wal differ diff --git a/server/db.js b/server/db.js index 70eb356..10febfe 100644 --- a/server/db.js +++ b/server/db.js @@ -145,6 +145,10 @@ try { if (!lossNames.includes('civilian_killed')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_killed INTEGER NOT NULL DEFAULT 0') if (!lossNames.includes('civilian_wounded')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_wounded INTEGER NOT NULL DEFAULT 0') if (!lossNames.includes('updated_at')) db.exec('ALTER TABLE combat_losses ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))') + if (!lossNames.includes('drones')) db.exec('ALTER TABLE combat_losses ADD COLUMN drones INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('missiles')) db.exec('ALTER TABLE combat_losses ADD COLUMN missiles INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('helicopters')) db.exec('ALTER TABLE combat_losses ADD COLUMN helicopters INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('submarines')) db.exec('ALTER TABLE combat_losses ADD COLUMN submarines INTEGER NOT NULL DEFAULT 0') } catch (_) {} // 迁移:所有表添加 updated_at 用于数据回放 diff --git a/server/seed.js b/server/seed.js index 83a70be..282c8a6 100644 --- a/server/seed.js +++ b/server/seed.js @@ -149,9 +149,9 @@ function seed() { try { db.exec(` - INSERT OR REPLACE INTO combat_losses (side, bases_destroyed, bases_damaged, personnel_killed, personnel_wounded, civilian_killed, civilian_wounded, aircraft, warships, armor, vehicles) VALUES - ('us', 0, 27, 127, 384, 18, 52, 2, 0, 0, 8), - ('iran', 3, 8, 2847, 5620, 412, 1203, 24, 12, 18, 42); + INSERT OR REPLACE INTO combat_losses (side, bases_destroyed, bases_damaged, personnel_killed, personnel_wounded, civilian_killed, civilian_wounded, aircraft, warships, armor, vehicles, drones, missiles, helicopters, submarines) VALUES + ('us', 0, 27, 127, 384, 18, 52, 2, 0, 0, 8, 4, 12, 1, 0), + ('iran', 3, 8, 2847, 5620, 412, 1203, 24, 12, 18, 42, 28, 156, 8, 2); `) } catch (_) { db.exec(` diff --git a/server/situationData.js b/server/situationData.js index 62cd1b9..0ba77e6 100644 --- a/server/situationData.js +++ b/server/situationData.js @@ -20,6 +20,10 @@ function toLosses(row) { warships: row.warships, armor: row.armor, vehicles: row.vehicles, + drones: row.drones ?? 0, + missiles: row.missiles ?? 0, + helicopters: row.helicopters ?? 0, + submarines: row.submarines ?? 0, } } @@ -31,6 +35,10 @@ const defaultLosses = { warships: 0, armor: 0, vehicles: 0, + drones: 0, + missiles: 0, + helicopters: 0, + submarines: 0, } function getSituation() { diff --git a/src/components/CombatLossesPanel.tsx b/src/components/CombatLossesPanel.tsx index 7c9d66a..a01387a 100644 --- a/src/components/CombatLossesPanel.tsx +++ b/src/components/CombatLossesPanel.tsx @@ -6,6 +6,10 @@ import { Ship, Shield, Car, + Scan, + Rocket, + Wind, + Anchor, TrendingDown, UserCircle, Activity, @@ -31,10 +35,14 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia { label: '战舰', icon: Ship, iconColor: 'text-blue-500', us: usLosses.warships, ir: iranLosses.warships }, { label: '装甲', icon: Shield, iconColor: 'text-emerald-500', us: usLosses.armor, ir: iranLosses.armor }, { label: '车辆', icon: Car, iconColor: 'text-slate-400', us: usLosses.vehicles, ir: iranLosses.vehicles }, + { label: '无人机', icon: Scan, iconColor: 'text-violet-400', us: usLosses.drones ?? 0, ir: iranLosses.drones ?? 0 }, + { label: '导弹', icon: Rocket, iconColor: 'text-orange-500', us: usLosses.missiles ?? 0, ir: iranLosses.missiles ?? 0 }, + { label: '直升机', icon: Wind, iconColor: 'text-teal-400', us: usLosses.helicopters ?? 0, ir: iranLosses.helicopters ?? 0 }, + { label: '潜艇', icon: Anchor, iconColor: 'text-indigo-400', us: usLosses.submarines ?? 0, ir: iranLosses.submarines ?? 0 }, ] return ( -
+
战损 @@ -46,7 +54,7 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia )}
-
+
{/* 人员伤亡 - 单独容器 */}
@@ -57,14 +65,14 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia
- {formatMillions(usLosses.personnelCasualties.killed)} + {formatMillions(usLosses.personnelCasualties.killed)} / - {formatMillions(usLosses.personnelCasualties.wounded)} + {formatMillions(usLosses.personnelCasualties.wounded)}
- {formatMillions(iranLosses.personnelCasualties.killed)} + {formatMillions(iranLosses.personnelCasualties.killed)} / - {formatMillions(iranLosses.personnelCasualties.wounded)} + {formatMillions(iranLosses.personnelCasualties.wounded)}
@@ -75,36 +83,36 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia 平民伤亡(合计)
-
+
- {formatMillions(civ.killed)} + {formatMillions(civ.killed)} / - {formatMillions(civ.wounded)} + {formatMillions(civ.wounded)}
- {/* 其它 - 标签+图标+数字,单独容器 */} -
+ {/* 其它 - 标签+图标+数字,竖屏横屏均完整显示,自适应排版 */} +
美:伊
-
+
{otherRows.map(({ label, icon: Icon, iconColor, ...rest }, i) => ( -
- - +
+ + {label} {'value' in rest ? ( - {rest.value} + {String(rest.value)} ) : ( - - {rest.us} + + {rest.us} : - {rest.ir} + {rest.ir} )}
diff --git a/src/components/HeaderPanel.tsx b/src/components/HeaderPanel.tsx index 5bb96f5..b7f1d89 100644 --- a/src/components/HeaderPanel.tsx +++ b/src/components/HeaderPanel.tsx @@ -133,15 +133,15 @@ export function HeaderPanel() { } return ( -
-
-

+
+
+

美伊军事态势显示

-
-
- - {formatDateTime(now)} +
+
+ + {formatDateTime(now)}
{(isConnected || isReplayMode) && ( @@ -150,8 +150,8 @@ export function HeaderPanel() { )}
-
-
+
+
在看 {viewers} | @@ -160,48 +160,41 @@ export function HeaderPanel() { - {isConnected ? ( - <> - - 实时 - - ) : ( - <> - - 已断开 - - )} + + {isConnected ? : } + {isConnected ? '实时' : '已断开'} +
-
-
- 国力对比 -
+
+
+ 国力对比 +
-
+
美 {usForces.powerIndex.overall} 伊 {iranForces.powerIndex.overall}
-
+
{/* 留言弹窗 */} diff --git a/src/components/WarMap.tsx b/src/components/WarMap.tsx index e6a008b..5f06240 100644 --- a/src/components/WarMap.tsx +++ b/src/components/WarMap.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect, useRef } from 'react' +import { useMemo, useEffect, useRef, useCallback } 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' @@ -20,6 +20,11 @@ const MAPBOX_TOKEN = config.mapboxAccessToken || '' // 相关区域 bbox:伊朗、以色列、胡塞区 (minLng, minLat, maxLng, maxLat),覆盖红蓝区域 const THEATER_BBOX = [22, 11, 64, 41] as const +/** 移动端/小屏时 fitBounds 使区域完整显示 */ +const THEATER_BOUNDS: [[number, number], [number, number]] = [ + [THEATER_BBOX[0], THEATER_BBOX[1]], + [THEATER_BBOX[2], THEATER_BBOX[3]], +] const THEATER_CENTER = { longitude: (THEATER_BBOX[0] + THEATER_BBOX[2]) / 2, latitude: (THEATER_BBOX[1] + THEATER_BBOX[3]) / 2, @@ -124,6 +129,7 @@ const FLIGHT_DURATION_MS = 2500 // 光点飞行单程时间 export function WarMap() { const mapRef = useRef(null) + const containerRef = useRef(null) const animRef = useRef(0) const startRef = useRef(0) const attackPathsRef = useRef<[number, number][][]>([]) @@ -433,6 +439,21 @@ export function WarMap() { return () => cancelAnimationFrame(animRef.current) }, []) + // 容器尺寸变化时 fitBounds,保证区域完整显示(移动端自适应) + const fitToTheater = useCallback(() => { + const map = mapRef.current?.getMap() + if (!map) return + map.fitBounds(THEATER_BOUNDS, { padding: 32, maxZoom: 6, duration: 0 }) + }, []) + + useEffect(() => { + const el = containerRef.current + if (!el) return + const ro = new ResizeObserver(() => fitToTheater()) + ro.observe(el) + return () => ro.disconnect() + }, [fitToTheater]) + if (!MAPBOX_TOKEN) { return (
@@ -455,7 +476,7 @@ export function WarMap() { } return ( -
+
{/* 图例 - 随容器自适应,避免遮挡 */}
@@ -502,7 +523,7 @@ export function WarMap() { touchRotate={false} style={{ width: '100%', height: '100%' }} onLoad={(e) => { - // 地图加载完成后启动动画;延迟确保 Source/Layer 已挂载 + fitToTheater() setTimeout(() => initAnimation.current(e.target), 150) }} > diff --git a/src/data/mockData.ts b/src/data/mockData.ts index e0c9ae0..aaef274 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -37,6 +37,11 @@ export interface CombatLosses { warships: number armor: number vehicles: number + /** 其它 */ + drones?: number + missiles?: number + helicopters?: number + submarines?: number } export interface SituationUpdate { @@ -152,6 +157,10 @@ export const INITIAL_MOCK_DATA: MilitarySituation = { warships: 0, armor: 0, vehicles: 8, + drones: 4, + missiles: 12, + helicopters: 1, + submarines: 0, }, wallStreetInvestmentTrend: [ { time: '2025-03-01T00:00:00', value: 82 }, @@ -202,6 +211,10 @@ export const INITIAL_MOCK_DATA: MilitarySituation = { warships: 12, armor: 18, vehicles: 42, + drones: 28, + missiles: 156, + helicopters: 8, + submarines: 2, }, retaliationSentiment: 78, retaliationSentimentHistory: [ diff --git a/src/hooks/useReplaySituation.ts b/src/hooks/useReplaySituation.ts index c73dcc9..3f5cb26 100644 --- a/src/hooks/useReplaySituation.ts +++ b/src/hooks/useReplaySituation.ts @@ -76,6 +76,10 @@ export function useReplaySituation(): MilitarySituation { warships: lerp(0, usLoss.warships), armor: lerp(0, usLoss.armor), vehicles: lerp(0, usLoss.vehicles), + drones: lerp(0, usLoss.drones ?? 0), + missiles: lerp(0, usLoss.missiles ?? 0), + helicopters: lerp(0, usLoss.helicopters ?? 0), + submarines: lerp(0, usLoss.submarines ?? 0), } const irLossesAt = { bases: { @@ -91,6 +95,10 @@ export function useReplaySituation(): MilitarySituation { warships: lerp(0, irLoss.warships), armor: lerp(0, irLoss.armor), vehicles: lerp(0, irLoss.vehicles), + drones: lerp(0, irLoss.drones ?? 0), + missiles: lerp(0, irLoss.missiles ?? 0), + helicopters: lerp(0, irLoss.helicopters ?? 0), + submarines: lerp(0, irLoss.submarines ?? 0), } // 被袭基地:按 damage_level 排序,高损毁先出现;根据 progress 决定显示哪些为 attacked diff --git a/src/index.css b/src/index.css index 443e932..7cf7aa8 100644 --- a/src/index.css +++ b/src/index.css @@ -61,3 +61,5 @@ body, to { transform: translateX(-50%); } } +/* 移动端横屏使用单列+滚动,不再做 zoom 缩放,保持比例正常 */ + diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 324aeb5..b24bb91 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -24,7 +24,7 @@ export function Dashboard() { }, []) return ( -
+
{lastError && (
{lastError}(使用本地缓存,请确保 API + WebSocket 已启动:npm run api) @@ -34,20 +34,20 @@ export function Dashboard() {
- {/* 竖屏/小屏:地图(100%宽) → 战损 → 左 → 右。横屏≥1280px:左|中|右 */} + {/* xl:左|中|右。竖屏:地图→战损→美国基地→伊朗基地→左→右 */}
-
+
- +