fix: 优化docker 镜像

This commit is contained in:
Daniel
2026-03-02 14:10:43 +08:00
parent 783a69dad1
commit 36576592a2
25 changed files with 491 additions and 58 deletions

View File

@@ -1,10 +1,19 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { StatCard } from './StatCard'
import { useSituationStore } from '@/store/situationStore'
import { useReplaySituation } from '@/hooks/useReplaySituation'
import { usePlaybackStore } from '@/store/playbackStore'
import { Wifi, WifiOff, Clock, Database } from 'lucide-react'
import { Wifi, WifiOff, Clock, Share2, Heart, Eye } from 'lucide-react'
const STORAGE_LIKES = 'us-iran-dashboard-likes'
function getStoredLikes(): number {
try {
return parseInt(localStorage.getItem(STORAGE_LIKES) ?? '0', 10)
} catch {
return 0
}
}
export function HeaderPanel() {
const situation = useReplaySituation()
@@ -12,12 +21,64 @@ export function HeaderPanel() {
const isReplayMode = usePlaybackStore((s) => s.isReplayMode)
const { usForces, iranForces } = situation
const [now, setNow] = useState(() => new Date())
const [likes, setLikes] = useState(getStoredLikes)
const [liked, setLiked] = useState(false)
const [viewers, setViewers] = useState(0)
const [cumulative, setCumulative] = useState(0)
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000)
return () => clearInterval(timer)
}, [])
const fetchStats = async () => {
try {
const res = await fetch('/api/visit', { method: 'POST' })
const data = await res.json()
if (data.viewers != null) setViewers(data.viewers)
if (data.cumulative != null) setCumulative(data.cumulative)
} catch {
setViewers((v) => (v > 0 ? v : 0))
setCumulative((c) => (c > 0 ? c : 0))
}
}
useEffect(() => {
fetchStats()
const t = setInterval(fetchStats, 30000)
return () => clearInterval(t)
}, [])
const handleShare = async () => {
const url = window.location.href
const title = '美伊军事态势显示'
if (typeof navigator.share === 'function') {
try {
await navigator.share({ title, url })
} catch (e) {
if ((e as Error).name !== 'AbortError') {
await copyToClipboard(url)
}
}
} else {
await copyToClipboard(url)
}
}
const copyToClipboard = (text: string) => {
return navigator.clipboard?.writeText(text) ?? Promise.resolve()
}
const handleLike = () => {
if (liked) return
setLiked(true)
const next = likes + 1
setLikes(next)
try {
localStorage.setItem(STORAGE_LIKES, String(next))
} catch {}
}
const formatDateTime = (d: Date) =>
d.toLocaleString('zh-CN', {
year: 'numeric',
@@ -59,14 +120,33 @@ export function HeaderPanel() {
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-3">
<Link
to="/db"
<div className="flex shrink-0 items-center gap-4">
<div className="flex items-center gap-2 text-military-text-secondary">
<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>
<span className="text-[10px]"> <b className="tabular-nums">{cumulative}</b></span>
</div>
<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"
>
<Database className="h-3 w-3" />
</Link>
<Share2 className="h-3 w-3" />
</button>
<button
type="button"
onClick={handleLike}
className={`flex items-center gap-1 rounded border px-2 py-1 text-[10px] transition-colors ${
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' : ''}`} />
{likes > 0 && <span className="tabular-nums">{likes}</span>}
</button>
{isConnected ? (
<>
<Wifi className="h-3.5 w-3.5 text-green-500" />

View File

@@ -286,6 +286,8 @@ export function WarMap() {
const tick = (t: number) => {
const elapsed = t - startRef.current
const zoom = map.getZoom()
const zoomScale = Math.max(0.5, zoom / 4.2) // 随镜头缩放放大变大、缩小变小4.2 为默认 zoom
try {
// 光点从起点飞向目标的循环动画
const src = map.getSource('attack-dots') as { setData: (d: GeoJSON.FeatureCollection) => void } | undefined
@@ -307,11 +309,11 @@ export function WarMap() {
const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.003)
map.setPaintProperty('points-damaged', 'circle-opacity', blink)
}
// attacked: 红色脉冲 2s 循环, 扩散半径 0→40px, opacity 1→0 (map.md)
// attacked: 红色脉冲 2s 循环, 半径随 zoom 缩放
if (map.getLayer('points-attacked-pulse')) {
const cycle = 2000
const phase = (elapsed % cycle) / cycle
const r = 40 * phase
const r = 40 * phase * zoomScale
const opacity = 1 - phase
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
@@ -376,11 +378,11 @@ export function WarMap() {
)
israelSrc.setData({ type: 'FeatureCollection', features })
}
// 伊朗被打击目标:蓝色脉冲 (2s 周期)
// 伊朗被打击目标:蓝色脉冲 (2s 周期), 半径随 zoom 缩放
if (map.getLayer('allied-strike-targets-pulse')) {
const cycle = 2000
const phase = (elapsed % cycle) / cycle
const r = 35 * phase
const r = 35 * phase * zoomScale
const opacity = 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)
@@ -390,11 +392,11 @@ export function WarMap() {
const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.004)
map.setPaintProperty('gdelt-events-orange', 'circle-opacity', blink)
}
// GDELT 红色 (710):脉冲扩散
// GDELT 红色 (710):脉冲扩散, 半径随 zoom 缩放
if (map.getLayer('gdelt-events-red-pulse')) {
const cycle = 2200
const phase = (elapsed % cycle) / cycle
const r = 30 * phase
const r = 30 * phase * zoomScale
const opacity = 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)