+
+
+ 数据库
+
{isConnected ? (
<>
diff --git a/src/data/mockData.ts b/src/data/mockData.ts
index 6e7885e..e0c9ae0 100644
--- a/src/data/mockData.ts
+++ b/src/data/mockData.ts
@@ -109,6 +109,8 @@ export interface MilitarySituation {
conflictEvents?: ConflictEvent[]
/** 战损统计(展示用) */
conflictStats?: ConflictStats
+ /** 平民伤亡合计(不区分阵营) */
+ civilianCasualtiesTotal?: { killed: number; wounded: number }
}
export const INITIAL_MOCK_DATA: MilitarySituation = {
@@ -246,4 +248,5 @@ export const INITIAL_MOCK_DATA: MilitarySituation = {
],
conflictEvents: [],
conflictStats: { total_events: 0, high_impact_events: 0, estimated_casualties: 0, estimated_strike_count: 0 },
+ civilianCasualtiesTotal: { killed: 430, wounded: 1255 },
}
diff --git a/src/hooks/useReplaySituation.ts b/src/hooks/useReplaySituation.ts
index 5ab9095..c73dcc9 100644
--- a/src/hooks/useReplaySituation.ts
+++ b/src/hooks/useReplaySituation.ts
@@ -61,8 +61,7 @@ export function useReplaySituation(): MilitarySituation {
const lerp = (a: number, b: number) => Math.round(a + progress * (b - a))
const usLoss = situation.usForces.combatLosses
const irLoss = situation.iranForces.combatLosses
- const civUs = usLoss.civilianCasualties ?? { killed: 0, wounded: 0 }
- const civIr = irLoss.civilianCasualties ?? { killed: 0, wounded: 0 }
+ const civTotal = situation.civilianCasualtiesTotal ?? { killed: 0, wounded: 0 }
const usLossesAt = {
bases: {
destroyed: lerp(0, usLoss.bases.destroyed),
@@ -72,7 +71,7 @@ export function useReplaySituation(): MilitarySituation {
killed: lerp(0, usLoss.personnelCasualties.killed),
wounded: lerp(0, usLoss.personnelCasualties.wounded),
},
- civilianCasualties: { killed: lerp(0, civUs.killed), wounded: lerp(0, civUs.wounded) },
+ civilianCasualties: { killed: 0, wounded: 0 },
aircraft: lerp(0, usLoss.aircraft),
warships: lerp(0, usLoss.warships),
armor: lerp(0, usLoss.armor),
@@ -87,7 +86,7 @@ export function useReplaySituation(): MilitarySituation {
killed: lerp(0, irLoss.personnelCasualties.killed),
wounded: lerp(0, irLoss.personnelCasualties.wounded),
},
- civilianCasualties: { killed: lerp(0, civIr.killed), wounded: lerp(0, civIr.wounded) },
+ civilianCasualties: { killed: 0, wounded: 0 },
aircraft: lerp(0, irLoss.aircraft),
warships: lerp(0, irLoss.warships),
armor: lerp(0, irLoss.armor),
@@ -115,6 +114,10 @@ export function useReplaySituation(): MilitarySituation {
return {
...situation,
lastUpdated: playbackTime,
+ civilianCasualtiesTotal: {
+ killed: lerp(0, civTotal.killed),
+ wounded: lerp(0, civTotal.wounded),
+ },
usForces: {
...situation.usForces,
keyLocations: usLocsAt,
diff --git a/src/index.css b/src/index.css
index 1dcaed8..b1607aa 100644
--- a/src/index.css
+++ b/src/index.css
@@ -31,6 +31,11 @@ body,
font-family: 'Orbitron', sans-serif;
}
+/* 数据库面板:易读字体 */
+.font-db {
+ font-family: 'Noto Sans SC', system-ui, -apple-system, sans-serif;
+}
+
/* Tabular numbers for aligned stat display */
.tabular-nums {
font-variant-numeric: tabular-nums;
diff --git a/src/main.tsx b/src/main.tsx
index 234dd89..6610f4b 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
import './index.css'
createRoot(document.getElementById('root')!).render(
-
+
+
+
)
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index e3d9779..72eea8c 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -68,6 +68,7 @@ export function Dashboard() {
usLosses={situation.usForces.combatLosses}
iranLosses={situation.iranForces.combatLosses}
conflictStats={situation.conflictStats}
+ civilianTotal={situation.civilianCasualtiesTotal}
className="min-w-0 flex-1 py-1"
/>
diff --git a/src/pages/DbDashboard.tsx b/src/pages/DbDashboard.tsx
new file mode 100644
index 0000000..1c4f8d7
--- /dev/null
+++ b/src/pages/DbDashboard.tsx
@@ -0,0 +1,161 @@
+import { useEffect, useState } from 'react'
+import { Database, Table, ArrowLeft, RefreshCw } from 'lucide-react'
+import { Link } from 'react-router-dom'
+
+interface TableData {
+ [table: string]: Record
[] | { error: string }
+}
+
+export function DbDashboard() {
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [expanded, setExpanded] = useState>(new Set(['situation_update', 'combat_losses', 'conflict_stats']))
+
+ useEffect(() => {
+ fetchData()
+ const t = setInterval(fetchData, 30000)
+ return () => clearInterval(t)
+ }, [])
+
+ const fetchData = async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ const res = await fetch('/api/db/dashboard')
+ if (!res.ok) throw new Error(res.statusText)
+ const json = await res.json()
+ setData(json)
+ } catch (e) {
+ setError(e instanceof Error ? e.message : '加载失败')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const toggle = (name: string) => {
+ setExpanded((s) => {
+ const next = new Set(s)
+ if (next.has(name)) next.delete(name)
+ else next.add(name)
+ return next
+ })
+ }
+
+ if (loading && !data) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ 返回主面板
+
+
+
+ 数据库内容
+
+
+
+
+
+ {error && (
+
+ {error}(请确保 API 已启动:npm run api)
+
+ )}
+
+
+ {data &&
+ Object.entries(data).map(([name, rows]) => {
+ const isExpanded = expanded.has(name)
+ const isError = rows && typeof rows === 'object' && 'error' in rows
+ const arr = Array.isArray(rows) ? rows : []
+ return (
+
+
+ {isExpanded && (
+
+ {isError ? (
+
{(rows as { error: string }).error}
+ ) : arr.length === 0 ? (
+
无数据
+ ) : (
+
+
+
+ {Object.keys(arr[0] as object).map((col) => (
+ |
+ {col}
+ |
+ ))}
+
+
+
+ {arr.map((row, i) => (
+
+ {Object.values(row as object).map((v, j) => (
+ |
+ {v === null || v === undefined
+ ? '—'
+ : typeof v === 'object'
+ ? JSON.stringify(v)
+ : String(v)}
+ |
+ ))}
+
+ ))}
+
+
+ )}
+
+ )}
+
+ )
+ })}
+
+
+ )
+}
diff --git a/src/store/situationStore.ts b/src/store/situationStore.ts
index a3c2d00..902dcc4 100644
--- a/src/store/situationStore.ts
+++ b/src/store/situationStore.ts
@@ -47,20 +47,36 @@ export function fetchAndSetSituation(): Promise {
}
let disconnectWs: (() => void) | null = null
+let pollInterval: ReturnType | null = null
+
+const POLL_INTERVAL_MS = 5000
+
+function pollSituation() {
+ fetchSituation()
+ .then((situation) => useSituationStore.getState().setSituation(situation))
+ .catch(() => {})
+}
export function startSituationWebSocket(): () => void {
- useSituationStore.getState().setConnected(true)
useSituationStore.getState().setLastError(null)
disconnectWs = connectSituationWebSocket((data) => {
+ useSituationStore.getState().setConnected(true)
useSituationStore.getState().setSituation(data as MilitarySituation)
})
+ pollSituation()
+ pollInterval = setInterval(pollSituation, POLL_INTERVAL_MS)
+
return stopSituationWebSocket
}
export function stopSituationWebSocket(): void {
disconnectWs?.()
disconnectWs = null
+ if (pollInterval) {
+ clearInterval(pollInterval)
+ pollInterval = null
+ }
useSituationStore.getState().setConnected(false)
}
diff --git a/start.sh b/start.sh
new file mode 100755
index 0000000..7ed6c5b
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+# 一键启动 US-Iran 态势面板:API + 前端 + 爬虫服务
+set -e
+cd "$(dirname "$0")"
+
+# 无 Ollama 时禁用 AI;GDELT 国内常超时,仅用 RSS 更新
+export CLEANER_AI_DISABLED=1
+export PARSER_AI_DISABLED=1
+export GDELT_DISABLED=1
+export RSS_INTERVAL_SEC=60
+
+echo "==> Checking dependencies..."
+[ ! -d node_modules ] && npm install
+
+echo "==> Checking crawler Python deps..."
+pip install -q -r crawler/requirements.txt 2>/dev/null || true
+
+echo "==> Seeding database (if needed)..."
+[ ! -f server/data.db ] && npm run api:seed
+
+echo "==> Starting API (http://localhost:3001)..."
+npm run api &
+API_PID=$!
+
+# 等待 API 就绪后再启动爬虫
+sleep 2
+
+echo "==> Starting GDELT/RSS crawler (http://localhost:8000)..."
+npm run gdelt &
+GDELT_PID=$!
+
+echo "==> Starting frontend (Vite dev server)..."
+npm run dev &
+DEV_PID=$!
+
+cleanup() {
+ echo ""
+ echo "==> Shutting down..."
+ kill $API_PID $GDELT_PID $DEV_PID 2>/dev/null || true
+ exit 0
+}
+trap cleanup SIGINT SIGTERM
+
+echo ""
+echo "==> All services running. Frontend: http://localhost:5173 | API: http://localhost:3001"
+echo " Press Ctrl+C to stop all."
+echo ""
+wait