fix:优化数据样式
This commit is contained in:
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,7 @@ import { usePlaybackStore } from '@/store/playbackStore'
|
||||
import { Wifi, WifiOff, Clock, Share2, Heart, Eye, MessageSquare } from 'lucide-react'
|
||||
|
||||
const STORAGE_LIKES = 'us-iran-dashboard-likes'
|
||||
// 冲突时长显示在 TimelinePanel(数据回放栏)内
|
||||
|
||||
function getStoredLikes(): number {
|
||||
try {
|
||||
@@ -155,13 +156,15 @@ export function HeaderPanel() {
|
||||
return (
|
||||
<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 className="min-w-0 shrink truncate text-sm font-bold uppercase tracking-wider text-military-accent sm:text-base sm:tracking-widest lg:text-2xl">
|
||||
美伊军事态势显示
|
||||
</h1>
|
||||
<div className="flex min-w-0 shrink-0 flex-col gap-0.5">
|
||||
<div className="flex w-[12rem] shrink-0 flex-col gap-0.5 font-mono sm:w-[13rem]">
|
||||
<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>
|
||||
<span className="min-w-[10rem] tabular-nums sm:min-w-[11rem]">
|
||||
{formatDateTime(now)}
|
||||
</span>
|
||||
</div>
|
||||
{(isConnected || isReplayMode) && (
|
||||
<span className={`text-[10px] ${isReplayMode ? 'text-military-accent' : 'text-green-500/90'}`}>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } 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'
|
||||
import { config } from '@/config'
|
||||
|
||||
/** 冲突开始时间:2月28日凌晨 03:00(本地时间) */
|
||||
const CONFLICT_START = new Date(2026, 1, 28, 3, 0, 0, 0)
|
||||
|
||||
function formatTick(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
return d.toLocaleString('zh-CN', {
|
||||
@@ -16,8 +19,17 @@ function formatTick(iso: string): string {
|
||||
})
|
||||
}
|
||||
|
||||
function getConflictDuration(toTime: Date): { days: number; hours: number } {
|
||||
const diffMs = toTime.getTime() - CONFLICT_START.getTime()
|
||||
if (diffMs <= 0) return { days: 0, hours: 0 }
|
||||
const days = Math.floor(diffMs / (24 * 60 * 60 * 1000))
|
||||
const hours = Math.floor((diffMs % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000))
|
||||
return { days, hours }
|
||||
}
|
||||
|
||||
export function TimelinePanel() {
|
||||
const situation = useSituationStore((s) => s.situation)
|
||||
const [now, setNow] = useState(() => new Date())
|
||||
const {
|
||||
isReplayMode,
|
||||
playbackTime,
|
||||
@@ -33,6 +45,14 @@ export function TimelinePanel() {
|
||||
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setNow(new Date()), 1000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
const toTime = isReplayMode ? new Date(playbackTime) : now
|
||||
const conflictDuration = getConflictDuration(toTime)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying || !isReplayMode) {
|
||||
if (timerRef.current) {
|
||||
@@ -64,8 +84,18 @@ export function TimelinePanel() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shrink-0 border-b border-military-border bg-military-panel/95 px-3 py-2">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="relative shrink-0 border-b border-military-border bg-military-panel/95 px-3 py-2">
|
||||
{!isReplayMode && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 flex items-center justify-center px-2"
|
||||
aria-hidden
|
||||
>
|
||||
<span className="tabular-nums font-bold text-red-500">
|
||||
冲突已持续 {conflictDuration.days} 天 {conflictDuration.hours} 小时
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReplayMode(!isReplayMode)}
|
||||
|
||||
@@ -316,12 +316,12 @@ export function WarMap() {
|
||||
const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.003)
|
||||
map.setPaintProperty('points-damaged', 'circle-opacity', blink)
|
||||
}
|
||||
// attacked: 红色脉冲 2s 循环, 半径随 zoom 缩放
|
||||
// attacked: 红色脉冲 2s 循环, 半径随 zoom 缩放;phase/r/opacity 钳位避免浮点或取模越界
|
||||
if (map.getLayer('points-attacked-pulse')) {
|
||||
const cycle = 2000
|
||||
const phase = (elapsed % cycle) / cycle
|
||||
const r = 40 * phase * zoomScale
|
||||
const opacity = 1 - phase
|
||||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||||
const r = Math.max(0, 40 * phase * zoomScale)
|
||||
const opacity = Math.min(1, Math.max(0, 1 - phase))
|
||||
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
|
||||
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
|
||||
}
|
||||
@@ -385,12 +385,12 @@ export function WarMap() {
|
||||
)
|
||||
israelSrc.setData({ type: 'FeatureCollection', features })
|
||||
}
|
||||
// 伊朗被打击目标:蓝色脉冲 (2s 周期), 半径随 zoom 缩放
|
||||
// 伊朗被打击目标:蓝色脉冲 (2s 周期), 半径随 zoom 缩放;phase/r/opacity 钳位
|
||||
if (map.getLayer('allied-strike-targets-pulse')) {
|
||||
const cycle = 2000
|
||||
const phase = (elapsed % cycle) / cycle
|
||||
const r = 35 * phase * zoomScale
|
||||
const opacity = Math.max(0, 1 - phase * 1.2)
|
||||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||||
const r = Math.max(0, 35 * phase * zoomScale)
|
||||
const opacity = Math.min(1, Math.max(0, 1 - phase * 1.2))
|
||||
map.setPaintProperty('allied-strike-targets-pulse', 'circle-radius', r)
|
||||
map.setPaintProperty('allied-strike-targets-pulse', 'circle-opacity', opacity)
|
||||
}
|
||||
@@ -399,12 +399,12 @@ export function WarMap() {
|
||||
const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.004)
|
||||
map.setPaintProperty('gdelt-events-orange', 'circle-opacity', blink)
|
||||
}
|
||||
// GDELT 红色 (7–10):脉冲扩散, 半径随 zoom 缩放
|
||||
// GDELT 红色 (7–10):脉冲扩散, 半径随 zoom 缩放;phase/r/opacity 钳位
|
||||
if (map.getLayer('gdelt-events-red-pulse')) {
|
||||
const cycle = 2200
|
||||
const phase = (elapsed % cycle) / cycle
|
||||
const r = 30 * phase * zoomScale
|
||||
const opacity = Math.max(0, 1 - phase * 1.1)
|
||||
const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
|
||||
const r = Math.max(0, 30 * phase * zoomScale)
|
||||
const opacity = Math.min(1, Math.max(0, 1 - phase * 1.1))
|
||||
map.setPaintProperty('gdelt-events-red-pulse', 'circle-radius', r)
|
||||
map.setPaintProperty('gdelt-events-red-pulse', 'circle-opacity', opacity)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user