fix: 优化docker 镜像
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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 红色 (7–10):脉冲扩散
|
||||
// GDELT 红色 (7–10):脉冲扩散, 半径随 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)
|
||||
|
||||
Reference in New Issue
Block a user