fix: 优化自适应界面
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 用于数据回放
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 (
|
||||
<div className={`flex min-h-[220px] max-h-[240px] min-w-0 flex-1 flex-col overflow-hidden rounded border border-military-border bg-military-panel/95 font-orbitron ${className}`}>
|
||||
<div className={`flex min-h-[200px] min-w-0 flex-1 flex-col overflow-auto rounded border border-military-border bg-military-panel/95 font-orbitron ${className}`}>
|
||||
<div className="mb-1.5 flex shrink-0 items-center justify-center gap-2 text-[10px] uppercase tracking-wider text-military-text-secondary">
|
||||
<TrendingDown className="h-2.5 w-2.5 shrink-0 text-amber-400" />
|
||||
战损
|
||||
@@ -46,7 +54,7 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden px-3 pb-2">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-visible px-3 pb-2">
|
||||
{/* 人员伤亡 - 单独容器 */}
|
||||
<div className="flex shrink-0 flex-col justify-center overflow-hidden rounded-lg border border-red-900/50 bg-red-950/40 px-3 py-2">
|
||||
<div className="mb-1 flex shrink-0 items-center justify-center gap-3 text-[9px] text-military-text-secondary">
|
||||
@@ -57,14 +65,14 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia
|
||||
</div>
|
||||
<div className="grid min-w-0 grid-cols-2 gap-x-2 gap-y-1 overflow-hidden text-center tabular-nums sm:gap-x-4">
|
||||
<div className="min-w-0 truncate text-military-us" title={`美: ${formatMillions(usLosses.personnelCasualties.killed)} / ${formatMillions(usLosses.personnelCasualties.wounded)}`}>
|
||||
<span className="text-base font-bold text-red-500">{formatMillions(usLosses.personnelCasualties.killed)}</span>
|
||||
<span className="text-sm font-bold text-red-500 sm:text-base">{formatMillions(usLosses.personnelCasualties.killed)}</span>
|
||||
<span className="mx-0.5 text-military-text-secondary">/</span>
|
||||
<span className="text-base font-semibold text-amber-500">{formatMillions(usLosses.personnelCasualties.wounded)}</span>
|
||||
<span className="text-sm font-semibold text-amber-500 sm:text-base">{formatMillions(usLosses.personnelCasualties.wounded)}</span>
|
||||
</div>
|
||||
<div className="min-w-0 truncate text-military-iran" title={`伊: ${formatMillions(iranLosses.personnelCasualties.killed)} / ${formatMillions(iranLosses.personnelCasualties.wounded)}`}>
|
||||
<span className="text-base font-bold text-red-500">{formatMillions(iranLosses.personnelCasualties.killed)}</span>
|
||||
<span className="text-sm font-bold text-red-500 sm:text-base">{formatMillions(iranLosses.personnelCasualties.killed)}</span>
|
||||
<span className="mx-0.5 text-military-text-secondary">/</span>
|
||||
<span className="text-base font-semibold text-amber-500">{formatMillions(iranLosses.personnelCasualties.wounded)}</span>
|
||||
<span className="text-sm font-semibold text-amber-500 sm:text-base">{formatMillions(iranLosses.personnelCasualties.wounded)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,36 +83,36 @@ export function CombatLossesPanel({ usLosses, iranLosses, conflictStats, civilia
|
||||
<UserCircle className="h-3 w-3 text-amber-400" />
|
||||
平民伤亡(合计)
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3 text-center tabular-nums">
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 text-center tabular-nums sm:gap-3">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Skull className="h-3 w-3 text-red-500" />
|
||||
<span className="text-base font-bold text-red-500">{formatMillions(civ.killed)}</span>
|
||||
<span className="text-sm font-bold text-red-500 sm:text-base">{formatMillions(civ.killed)}</span>
|
||||
</span>
|
||||
<span className="text-military-text-secondary/60">/</span>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Bandage className="h-3 w-3 text-amber-500" />
|
||||
<span className="text-base font-semibold text-amber-500">{formatMillions(civ.wounded)}</span>
|
||||
<span className="text-sm font-semibold text-amber-500 sm:text-base">{formatMillions(civ.wounded)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 其它 - 标签+图标+数字,单独容器 */}
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden rounded border border-military-border/50 bg-military-dark/30 px-2 py-1.5">
|
||||
{/* 其它 - 标签+图标+数字,竖屏横屏均完整显示,自适应排版 */}
|
||||
<div className="min-w-0 shrink-0 rounded border border-military-border/50 bg-military-dark/30 px-2 py-1.5">
|
||||
<div className="mb-1 text-[8px] text-military-text-secondary">美:伊</div>
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-0.5 overflow-hidden text-[11px] tabular-nums lg:grid-cols-3">
|
||||
<div className="grid min-w-0 grid-cols-2 gap-x-2 gap-y-1 tabular-nums sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
|
||||
{otherRows.map(({ label, icon: Icon, iconColor, ...rest }, i) => (
|
||||
<div key={i} className="flex min-w-0 items-center justify-between gap-1 overflow-hidden">
|
||||
<span className="flex shrink-0 items-center gap-0.5 text-military-text-primary">
|
||||
<Icon className={`h-3 w-3 ${iconColor}`} />
|
||||
<div key={i} className="flex min-w-0 items-center justify-between gap-1">
|
||||
<span className="flex shrink-0 items-center gap-0.5 text-military-text-primary text-[9px] sm:text-[10px]">
|
||||
<Icon className={`h-2.5 w-2.5 shrink-0 sm:h-3 sm:w-3 ${iconColor}`} />
|
||||
{label}
|
||||
</span>
|
||||
{'value' in rest ? (
|
||||
<span className="min-w-0 truncate text-right text-amber-400">{rest.value}</span>
|
||||
<span className="min-w-0 text-right text-[9px] text-amber-400 sm:text-[10px] tabular-nums">{String(rest.value)}</span>
|
||||
) : (
|
||||
<span className="min-w-0 truncate text-right">
|
||||
<span className="text-military-us">{rest.us}</span>
|
||||
<span className="min-w-0 shrink-0 text-right text-[9px] sm:text-[10px]">
|
||||
<span className="text-military-us tabular-nums">{rest.us}</span>
|
||||
<span className="text-military-text-secondary/60">:</span>
|
||||
<span className="text-military-iran">{rest.ir}</span>
|
||||
<span className="text-military-iran tabular-nums">{rest.ir}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -133,15 +133,15 @@ export function HeaderPanel() {
|
||||
}
|
||||
|
||||
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">
|
||||
<header className="flex shrink-0 flex-wrap items-center justify-between gap-2 overflow-hidden border-b border-military-border bg-military-panel/95 px-2 py-2 font-orbitron sm:gap-3 sm:px-4 sm:py-3 lg:flex-nowrap lg:gap-4 lg:px-6">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2 sm:gap-3 lg:gap-6">
|
||||
<h1 className="truncate text-sm font-bold uppercase tracking-wider text-military-accent sm:text-base sm:tracking-widest lg:text-2xl">
|
||||
美伊军事态势显示
|
||||
</h1>
|
||||
<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 className="flex min-w-0 shrink-0 flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1.5 text-xs text-military-text-secondary sm:gap-2 sm:text-sm">
|
||||
<Clock className="h-3.5 w-3.5 shrink-0 sm:h-4 sm:w-4" />
|
||||
<span className="tabular-nums sm:min-w-[10rem]">{formatDateTime(now)}</span>
|
||||
</div>
|
||||
{(isConnected || isReplayMode) && (
|
||||
<span className={`text-[10px] ${isReplayMode ? 'text-military-accent' : 'text-green-500/90'}`}>
|
||||
@@ -150,8 +150,8 @@ export function HeaderPanel() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-military-text-secondary">
|
||||
<div className="flex min-w-0 shrink flex-wrap items-center justify-end gap-2 sm:gap-3 lg:shrink-0 lg:gap-4">
|
||||
<div className="flex shrink-0 items-center gap-1.5 text-military-text-secondary sm:gap-2">
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
<span className="text-[10px]">在看 <b className="text-military-accent tabular-nums">{viewers}</b></span>
|
||||
<span className="text-[10px] opacity-70">|</span>
|
||||
@@ -160,48 +160,41 @@ export function HeaderPanel() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFeedbackOpen(true)}
|
||||
className="flex items-center gap-1 rounded border border-military-border px-2 py-1 text-[10px] text-military-text-secondary hover:bg-military-border/30 hover:text-cyan-400"
|
||||
className="flex shrink-0 items-center gap-1 rounded border border-military-border px-1.5 py-0.5 text-[9px] text-military-text-secondary hover:bg-military-border/30 hover:text-cyan-400 sm:px-2 sm:py-1 sm:text-[10px]"
|
||||
>
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
<MessageSquare className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||
留言
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShare}
|
||||
className="flex items-center gap-1 rounded border border-military-border px-2 py-1 text-[10px] text-military-text-secondary hover:bg-military-border/30 hover:text-cyan-400"
|
||||
className="flex shrink-0 items-center gap-1 rounded border border-military-border px-1.5 py-0.5 text-[9px] text-military-text-secondary hover:bg-military-border/30 hover:text-cyan-400 sm:px-2 sm:py-1 sm:text-[10px]"
|
||||
>
|
||||
<Share2 className="h-3 w-3" />
|
||||
<Share2 className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||
分享
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLike}
|
||||
className={`flex items-center gap-1 rounded border px-2 py-1 text-[10px] transition-colors ${
|
||||
className={`flex shrink-0 items-center gap-1 rounded border px-1.5 py-0.5 text-[9px] transition-colors sm:px-2 sm:py-1 sm:text-[10px] ${
|
||||
liked
|
||||
? 'border-red-500/50 bg-red-500/20 text-red-400'
|
||||
: 'border-military-border text-military-text-secondary hover:bg-military-border/30 hover:text-red-400'
|
||||
}`}
|
||||
>
|
||||
<Heart className={`h-3 w-3 ${liked ? 'fill-current' : ''}`} />
|
||||
<Heart className={`h-2.5 w-2.5 sm:h-3 sm:w-3 ${liked ? 'fill-current' : ''}`} />
|
||||
点赞 {likes > 0 && <span className="tabular-nums">{likes}</span>}
|
||||
</button>
|
||||
{isConnected ? (
|
||||
<>
|
||||
<Wifi className="h-3.5 w-3.5 text-green-500" />
|
||||
<span className="text-xs text-green-500">实时</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="h-3.5 w-3.5 text-military-text-secondary" />
|
||||
<span className="text-xs text-military-text-secondary">已断开</span>
|
||||
</>
|
||||
)}
|
||||
<span className={`flex items-center gap-1 ${isConnected ? 'text-green-500' : 'text-military-text-secondary'}`}>
|
||||
{isConnected ? <Wifi className="h-3.5 w-3.5" /> : <WifiOff className="h-3.5 w-3.5" />}
|
||||
<span className="text-xs">{isConnected ? '实时' : '已断开'}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 lg:gap-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase tracking-wider text-military-text-secondary">国力对比</span>
|
||||
<div className="mt-0.5 flex h-2 w-28 overflow-hidden rounded-full bg-military-border">
|
||||
<div className="flex min-w-0 shrink flex-wrap items-center gap-2 sm:gap-3 lg:gap-4">
|
||||
<div className="flex shrink-0 flex-col">
|
||||
<span className="text-[9px] uppercase tracking-wider text-military-text-secondary sm:text-[10px]">国力对比</span>
|
||||
<div className="mt-0.5 flex h-1.5 w-20 overflow-hidden rounded-full bg-military-border sm:h-2 sm:w-28">
|
||||
<div
|
||||
className="bg-military-us transition-all"
|
||||
style={{ width: `${(usForces.powerIndex.overall / (usForces.powerIndex.overall + iranForces.powerIndex.overall)) * 100}%` }}
|
||||
@@ -211,29 +204,29 @@ export function HeaderPanel() {
|
||||
style={{ width: `${(iranForces.powerIndex.overall / (usForces.powerIndex.overall + iranForces.powerIndex.overall)) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-0.5 flex justify-between text-[9px] tabular-nums text-military-text-secondary">
|
||||
<div className="mt-0.5 flex justify-between text-[8px] tabular-nums text-military-text-secondary sm:text-[9px]">
|
||||
<span className="text-military-us">美 {usForces.powerIndex.overall}</span>
|
||||
<span className="text-military-iran">伊 {iranForces.powerIndex.overall}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-8 w-px shrink-0 bg-military-border" />
|
||||
<div className="hidden h-8 w-px shrink-0 bg-military-border sm:block" />
|
||||
<StatCard
|
||||
label="美国/盟国"
|
||||
value={usForces.powerIndex.overall}
|
||||
variant="us"
|
||||
className="border-military-us/50"
|
||||
className="shrink-0 border-military-us/50 px-1.5 py-1 sm:px-2 sm:py-1.5"
|
||||
/>
|
||||
<StatCard
|
||||
label="伊朗"
|
||||
value={iranForces.powerIndex.overall}
|
||||
variant="iran"
|
||||
className="border-military-iran/50"
|
||||
className="shrink-0 border-military-iran/50 px-1.5 py-1 sm:px-2 sm:py-1.5"
|
||||
/>
|
||||
<StatCard
|
||||
label="差距"
|
||||
value={`+${usForces.powerIndex.overall - iranForces.powerIndex.overall}`}
|
||||
variant="accent"
|
||||
className="border-military-accent/50"
|
||||
className="shrink-0 border-military-accent/50 px-1.5 py-1 sm:px-2 sm:py-1.5"
|
||||
/>
|
||||
</div>
|
||||
{/* 留言弹窗 */}
|
||||
|
||||
@@ -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<MapRef>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const animRef = useRef<number>(0)
|
||||
const startRef = useRef<number>(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 (
|
||||
<div className="flex h-full w-full items-center justify-center bg-military-dark">
|
||||
@@ -455,7 +476,7 @@ export function WarMap() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div ref={containerRef} className="relative h-full w-full min-w-0">
|
||||
{/* 图例 - 随容器自适应,避免遮挡 */}
|
||||
<div className="absolute bottom-2 left-2 z-10 flex flex-wrap gap-x-3 gap-y-1 rounded bg-black/70 px-2 py-1.5 text-[9px] sm:text-[10px]">
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -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)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,3 +61,5 @@ body,
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
/* 移动端横屏使用单列+滚动,不再做 zoom 缩放,保持比例正常 */
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export function Dashboard() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full max-w-full min-h-0 flex-col overflow-hidden bg-military-dark font-orbitron">
|
||||
<div className="landscape-scaler flex h-screen w-full max-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)
|
||||
@@ -34,20 +34,20 @@ export function Dashboard() {
|
||||
<TimelinePanel />
|
||||
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-auto overflow-x-hidden xl:flex-row xl:overflow-hidden">
|
||||
{/* 竖屏/小屏:地图(100%宽) → 战损 → 左 → 右。横屏≥1280px:左|中|右 */}
|
||||
{/* xl:左|中|右。竖屏:地图→战损→美国基地→伊朗基地→左→右 */}
|
||||
<main className="flex w-full min-h-[280px] shrink-0 flex-col overflow-hidden xl:order-2 xl:min-h-0 xl:min-w-0 xl:flex-1 xl:shrink">
|
||||
<div className="h-[45vmin] min-h-[220px] w-full shrink-0 xl:min-h-0 xl:flex-1">
|
||||
<div className="h-[45vmin] min-h-[180px] w-full shrink-0 xl:min-h-0 xl:flex-1">
|
||||
<WarMap />
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-2 overflow-x-auto border-t border-military-border bg-military-panel/95 px-3 py-2 xl:flex-row xl:items-stretch xl:overflow-visible xl:px-4">
|
||||
<BaseStatusPanel keyLocations={situation.usForces.keyLocations} className="shrink-0 xl:min-w-[200px] xl:border-r xl:border-military-border xl:pr-4" />
|
||||
<CombatLossesPanel
|
||||
usLosses={situation.usForces.combatLosses}
|
||||
iranLosses={situation.iranForces.combatLosses}
|
||||
conflictStats={situation.conflictStats}
|
||||
civilianTotal={situation.civilianCasualtiesTotal}
|
||||
className="min-w-0 flex-1 py-1"
|
||||
className="min-w-0 flex-1 shrink-0 py-1"
|
||||
/>
|
||||
<BaseStatusPanel keyLocations={situation.usForces.keyLocations} className="shrink-0 xl:min-w-[200px] xl:border-r xl:border-military-border xl:pr-4" />
|
||||
<IranBaseStatusPanel
|
||||
keyLocations={situation.iranForces.keyLocations}
|
||||
className="min-w-0 shrink-0 xl:min-w-[200px]"
|
||||
|
||||
@@ -6,6 +6,9 @@ export default {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
'landscape-sm': { raw: '(orientation: landscape) and (max-height: 768px)' },
|
||||
},
|
||||
keyframes: {
|
||||
'vert-marquee': {
|
||||
'0%': { transform: 'translateY(0)' },
|
||||
|
||||
Reference in New Issue
Block a user