fix: 优化后端数据

This commit is contained in:
Daniel
2026-03-02 16:29:11 +08:00
parent 81628a136a
commit a9caf6e7c0
18 changed files with 295 additions and 15 deletions

View File

@@ -0,0 +1,72 @@
import { useMemo } from 'react'
import { MapPin, AlertTriangle, AlertCircle } from 'lucide-react'
import type { MilitarySituation } from '@/data/mockData'
interface IranBaseStatusPanelProps {
keyLocations: MilitarySituation['iranForces']['keyLocations']
className?: string
}
export function IranBaseStatusPanel({ keyLocations = [], className = '' }: IranBaseStatusPanelProps) {
const stats = useMemo(() => {
const bases = (keyLocations || []).filter((loc) => loc.type === 'Base' || loc.type === 'Port' || loc.type === 'Nuclear' || loc.type === 'Missile')
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 { total: bases.length, 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 text-amber-500" />
</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>{stats.total}</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="flex items-center gap-1 text-military-text-secondary">
<AlertTriangle className="h-3 w-3 text-amber-400" />
</span>
<strong className="text-amber-400">{stats.moderate}</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-300" />
</span>
<strong className="text-amber-300">{stats.light}</strong>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,56 @@
import { useMemo } from 'react'
import type { SituationUpdate, ConflictEvent } from '@/data/mockData'
import { processTickerText } from '@/utils/tickerText'
interface NewsTickerProps {
updates?: SituationUpdate[]
conflictEvents?: ConflictEvent[]
className?: string
}
export function NewsTicker({ updates = [], conflictEvents = [], className = '' }: NewsTickerProps) {
const items = useMemo(() => {
const list: { id: string; text: string }[] = []
for (const e of conflictEvents || []) {
const text = processTickerText(e.title || '')
if (text) list.push({ id: `ev-${e.event_id}`, text })
}
for (const u of updates || []) {
const text = processTickerText(u.summary || '')
if (text) list.push({ id: `up-${u.id}`, text })
}
return list.slice(0, 30)
}, [updates, conflictEvents])
const baseCls = 'flex items-center overflow-hidden'
const defaultCls = 'border-b border-military-border/50 bg-military-panel/60 py-1.5'
const wrapperCls = className ? `${baseCls} ${className}` : `${baseCls} ${defaultCls}`
if (items.length === 0) {
return (
<div className={wrapperCls}>
<span className="shrink-0 pr-2 text-[10px] uppercase text-military-text-secondary"></span>
<span className="text-[11px] text-military-text-secondary"></span>
</div>
)
}
const content = items.map((i) => i.text).join(' ◆ ')
const duration = Math.max(180, Math.min(480, content.length * 0.8))
return (
<div className={wrapperCls}>
<span className="shrink-0 pr-2 text-[10px] font-medium uppercase tracking-wider text-cyan-400"></span>
<div className="min-w-0 flex-1 overflow-hidden">
<div
className="inline-flex whitespace-nowrap text-[11px] text-military-text-secondary"
style={{ animation: `ticker ${duration}s linear infinite` }}
>
<span className="px-2">{content}</span>
<span className="px-2 text-cyan-400/60"></span>
<span className="px-2">{content}</span>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +1,8 @@
import { useEffect, useRef } from 'react'
import { Play, Pause, SkipBack, SkipForward, History } from 'lucide-react'
import { usePlaybackStore, REPLAY_TICKS, REPLAY_START, REPLAY_END } from '@/store/playbackStore'
import { useSituationStore } from '@/store/situationStore'
import { NewsTicker } from './NewsTicker'
function formatTick(iso: string): string {
const d = new Date(iso)
@@ -14,6 +16,7 @@ function formatTick(iso: string): string {
}
export function TimelinePanel() {
const situation = useSituationStore((s) => s.situation)
const {
isReplayMode,
playbackTime,
@@ -75,6 +78,16 @@ export function TimelinePanel() {
</button>
{!isReplayMode && (
<div className="min-w-0 flex-1">
<NewsTicker
updates={situation.recentUpdates}
conflictEvents={situation.conflictEvents}
className="!border-0 !bg-transparent !py-0 !px-0"
/>
</div>
)}
{isReplayMode && (
<>
<div className="flex items-center gap-1">

View File

@@ -53,3 +53,9 @@ body,
background: rgba(75, 85, 99, 0.5);
border-radius: 2px;
}
@keyframes ticker {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}

View File

@@ -4,7 +4,7 @@ import { TimelinePanel } from '@/components/TimelinePanel'
import { ForcePanel } from '@/components/ForcePanel'
import { WarMap } from '@/components/WarMap'
import { CombatLossesPanel } from '@/components/CombatLossesPanel'
import { EventTimelinePanel } from '@/components/EventTimelinePanel'
import { IranBaseStatusPanel } from '@/components/IranBaseStatusPanel'
import { BaseStatusPanel } from '@/components/BaseStatusPanel'
import { PowerChart } from '@/components/PowerChart'
import { InvestmentTrendChart } from '@/components/InvestmentTrendChart'
@@ -71,7 +71,10 @@ export function Dashboard() {
civilianTotal={situation.civilianCasualtiesTotal}
className="min-w-0 flex-1 py-1"
/>
<EventTimelinePanel updates={situation.recentUpdates} conflictEvents={situation.conflictEvents} className="min-w-0 shrink-0 min-h-[80px] overflow-hidden lg:min-w-[240px]" />
<IranBaseStatusPanel
keyLocations={situation.iranForces.keyLocations}
className="min-w-0 shrink-0 lg:min-w-[200px]"
/>
</div>
</main>

View File

@@ -49,7 +49,7 @@ export function fetchAndSetSituation(): Promise<void> {
let disconnectWs: (() => void) | null = null
let pollInterval: ReturnType<typeof setInterval> | null = null
const POLL_INTERVAL_MS = 5000
const POLL_INTERVAL_MS = 3000
function pollSituation() {
fetchSituation()

35
src/utils/tickerText.ts Normal file
View File

@@ -0,0 +1,35 @@
/**
* 滚动情报文本处理:转为简体中文,过滤非中文内容
*/
import { Converter } from 'opencc-js/t2cn'
const t2s = Converter({ from: 'twp', to: 'cn' })
/** 简体中文字符范围 */
const ZH_REGEX = /[\u4e00-\u9fff]/g
/** 文本中中文占比是否达标至少30% */
export function isMostlyChinese(text: string): boolean {
if (!text?.trim()) return false
const zh = text.match(ZH_REGEX)
const zhCount = zh ? zh.length : 0
return zhCount / text.length >= 0.3
}
/** 繁体转简体 */
export function toSimplifiedChinese(text: string): string {
if (!text?.trim()) return text
try {
return t2s(text)
} catch {
return text
}
}
/** 处理滚动情报项:转为简体,非中文为主则过滤 */
export function processTickerText(text: string): string | null {
const t = (text || '').trim()
if (!t) return null
if (!isMostlyChinese(t)) return null
return toSimplifiedChinese(t)
}