fix:优化整个大屏界面

This commit is contained in:
Daniel
2026-03-01 22:26:14 +08:00
parent c07fc681dd
commit 24d0593e12
8 changed files with 728 additions and 106 deletions

View File

@@ -4,15 +4,40 @@ import type { MapRef } from 'react-map-gl'
import type { Map as MapboxMap } from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { useSituationStore } from '@/store/situationStore'
import { ATTACKED_TARGETS } from '@/data/mapLocations'
import {
ATTACKED_TARGETS,
ALLIED_STRIKE_LOCATIONS,
LINCOLN_COORDS,
LINCOLN_STRIKE_TARGETS,
FORD_COORDS,
FORD_STRIKE_TARGETS,
ISRAEL_STRIKE_SOURCE,
ISRAEL_STRIKE_TARGETS,
} from '@/data/mapLocations'
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN || ''
const DEFAULT_VIEW = { longitude: 52.5, latitude: 26.5, zoom: 5.2 }
// 相关区域 bbox伊朗、以色列、胡塞区 (minLng, minLat, maxLng, maxLat),覆盖红蓝区域
const THEATER_BBOX = [22, 11, 64, 41] as const
const THEATER_CENTER = {
longitude: (THEATER_BBOX[0] + THEATER_BBOX[2]) / 2,
latitude: (THEATER_BBOX[1] + THEATER_BBOX[3]) / 2,
}
const DEFAULT_VIEW = { ...THEATER_CENTER, zoom: 4.2 }
const COUNTRIES_GEOJSON =
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson'
// 胡塞武装失控区 [lng, lat] 闭环
const HOUTHI_POLYGON: [number, number][] = [
[42.7, 15.8],
[43.3, 16.5],
[45.1, 17.2],
[45.8, 15.1],
[44.2, 13.5],
[42.7, 15.8],
]
const IRAN_ADMIN = 'Iran'
const ALLIES_ADMIN = [
'Qatar',
@@ -33,14 +58,39 @@ const ALLIES_ADMIN = [
// 伊朗攻击源 德黑兰 [lng, lat]
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
/** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */
function parabolaPath(
start: [number, number],
end: [number, number],
height = 2
height = 3
): [number, number][] {
const midLng = (start[0] + end[0]) / 2
const midLat = (start[1] + end[1]) / 2 + height
return [start, [midLng, midLat], end]
const ctrl: [number, number] = [
(start[0] + end[0]) / 2,
(start[1] + end[1]) / 2 + height,
]
// 生成多段点使曲线更平滑
const pts: [number, number][] = [start]
for (let i = 1; i < 12; i++) {
const s = i / 12
const t = 1 - s
const x = t * t * start[0] + 2 * t * s * ctrl[0] + s * s * end[0]
const y = t * t * start[1] + 2 * t * s * ctrl[1] + s * s * end[1]
pts.push([x, y])
}
pts.push(end)
return pts
}
/** 沿路径插值t ∈ [0,1],支持多点路径 */
function interpolateOnPath(path: [number, number][], t: number): [number, number] {
if (t <= 0) return path[0]
if (t >= 1) return path[path.length - 1]
const n = path.length - 1
const seg = Math.min(Math.floor(t * n), n - 1)
const u = (t * n) - seg
const a = path[seg]
const b = path[seg + 1]
return [a[0] + u * (b[0] - a[0]), a[1] + u * (b[1] - a[1])]
}
type BaseStatus = 'operational' | 'damaged' | 'attacked'
@@ -69,10 +119,16 @@ function toFeature(loc: KeyLoc, side: 'us' | 'iran', status?: BaseStatus) {
}
}
const FLIGHT_DURATION_MS = 2500 // 光点飞行单程时间
export function WarMap() {
const mapRef = useRef<MapRef>(null)
const animRef = useRef<number>(0)
const startRef = useRef<number>(0)
const attackPathsRef = useRef<[number, number][][]>([])
const lincolnPathsRef = useRef<[number, number][][]>([])
const fordPathsRef = useRef<[number, number][][]>([])
const israelPathsRef = useRef<[number, number][][]>([])
const { situation } = useSituationStore()
const { usForces, iranForces } = situation
@@ -113,20 +169,74 @@ export function WarMap() {
}
}, [usForces.keyLocations, iranForces.keyLocations])
// 德黑兰到 27 个被袭目标的攻击曲线
// 德黑兰到 27 个被袭目标的攻击路径(静态线条)
const attackPaths = useMemo(
() => ATTACKED_TARGETS.map((target) => parabolaPath(TEHRAN_SOURCE, target as [number, number])),
[]
)
attackPathsRef.current = attackPaths
const lincolnPaths = useMemo(
() => LINCOLN_STRIKE_TARGETS.map((t) => parabolaPath(LINCOLN_COORDS, t)),
[]
)
const fordPaths = useMemo(
() => FORD_STRIKE_TARGETS.map((t) => parabolaPath(FORD_COORDS, t)),
[]
)
const israelPaths = useMemo(
() => ISRAEL_STRIKE_TARGETS.map((t) => parabolaPath(ISRAEL_STRIKE_SOURCE, t)),
[]
)
lincolnPathsRef.current = lincolnPaths
fordPathsRef.current = fordPaths
israelPathsRef.current = israelPaths
const lincolnLinesGeoJson = useMemo(
() => ({
type: 'FeatureCollection' as const,
features: lincolnPaths.map((coords) => ({
type: 'Feature' as const,
properties: {},
geometry: { type: 'LineString' as const, coordinates: coords },
})),
}),
[lincolnPaths]
)
const fordLinesGeoJson = useMemo(
() => ({
type: 'FeatureCollection' as const,
features: fordPaths.map((coords) => ({
type: 'Feature' as const,
properties: {},
geometry: { type: 'LineString' as const, coordinates: coords },
})),
}),
[fordPaths]
)
const israelLinesGeoJson = useMemo(
() => ({
type: 'FeatureCollection' as const,
features: israelPaths.map((coords) => ({
type: 'Feature' as const,
properties: {},
geometry: { type: 'LineString' as const, coordinates: coords },
})),
}),
[israelPaths]
)
const attackLinesGeoJson = useMemo(
() => ({
type: 'FeatureCollection' as const,
features: ATTACKED_TARGETS.map((target) => ({
features: attackPaths.map((coords) => ({
type: 'Feature' as const,
properties: {},
geometry: {
type: 'LineString' as const,
coordinates: parabolaPath(TEHRAN_SOURCE, target as [number, number]),
},
geometry: { type: 'LineString' as const, coordinates: coords },
})),
}),
[]
[attackPaths]
)
const hideNonBelligerentLabels = (map: MapboxMap) => {
@@ -147,18 +257,27 @@ export function WarMap() {
}
}
useEffect(() => {
const map = mapRef.current?.getMap()
if (!map) return
const initAnimation = useRef<(map: MapboxMap) => void>(null!)
initAnimation.current = (map: MapboxMap) => {
startRef.current = performance.now()
const tick = (t: number) => {
const elapsed = t - startRef.current
try {
if (map.getLayer('attack-lines')) {
const offset = (elapsed / 16) * 0.8
map.setPaintProperty('attack-lines', 'line-dasharray', [2, 2])
map.setPaintProperty('attack-lines', 'line-dash-offset', -offset)
// 光点从起点飞向目标的循环动画
const src = map.getSource('attack-dots') as { setData: (d: GeoJSON.FeatureCollection) => void } | undefined
const paths = attackPathsRef.current
if (src && paths.length > 0) {
const features: GeoJSON.Feature<GeoJSON.Point>[] = paths.map((path, i) => {
const progress = ((elapsed / FLIGHT_DURATION_MS + i / paths.length) % 1)
const coord = interpolateOnPath(path, progress)
return {
type: 'Feature' as const,
properties: {},
geometry: { type: 'Point' as const, coordinates: coord },
}
})
src.setData({ type: 'FeatureCollection', features })
}
// damaged: 橙色闪烁 opacity 0.5 ~ 1, 约 1s 周期
if (map.getLayer('points-damaged')) {
@@ -174,21 +293,100 @@ export function WarMap() {
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
}
// 林肯号打击伊朗:蓝色光点
const lincolnSrc = map.getSource('allied-strike-dots-lincoln') as
| { setData: (d: GeoJSON.FeatureCollection) => void }
| undefined
const lincolnPaths = lincolnPathsRef.current
if (lincolnSrc && lincolnPaths.length > 0) {
const features: GeoJSON.Feature<GeoJSON.Point>[] = lincolnPaths.map(
(path, i) => {
const progress =
(elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(lincolnPaths.length, 1)) % 1
const coord = interpolateOnPath(path, progress)
return {
type: 'Feature' as const,
properties: {},
geometry: { type: 'Point' as const, coordinates: coord },
}
}
)
lincolnSrc.setData({ type: 'FeatureCollection', features })
}
// 福特号打击伊朗:青色光点
const fordSrc = map.getSource('allied-strike-dots-ford') as
| { setData: (d: GeoJSON.FeatureCollection) => void }
| undefined
const fordPaths = fordPathsRef.current
if (fordSrc && fordPaths.length > 0) {
const features: GeoJSON.Feature<GeoJSON.Point>[] = fordPaths.map(
(path, i) => {
const progress =
(elapsed / FLIGHT_DURATION_MS + 0.3 + i / Math.max(fordPaths.length, 1)) % 1
const coord = interpolateOnPath(path, progress)
return {
type: 'Feature' as const,
properties: {},
geometry: { type: 'Point' as const, coordinates: coord },
}
}
)
fordSrc.setData({ type: 'FeatureCollection', features })
}
// 以色列打击伊朗:浅青/白色光点
const israelSrc = map.getSource('allied-strike-dots-israel') as
| { setData: (d: GeoJSON.FeatureCollection) => void }
| undefined
const israelPaths = israelPathsRef.current
if (israelSrc && israelPaths.length > 0) {
const features: GeoJSON.Feature<GeoJSON.Point>[] = israelPaths.map(
(path, i) => {
const progress =
(elapsed / FLIGHT_DURATION_MS + 0.1 + i / Math.max(israelPaths.length, 1)) % 1
const coord = interpolateOnPath(path, progress)
return {
type: 'Feature' as const,
properties: {},
geometry: { type: 'Point' as const, coordinates: coord },
}
}
)
israelSrc.setData({ type: 'FeatureCollection', features })
}
// 伊朗被打击目标:蓝色脉冲 (2s 周期)
if (map.getLayer('allied-strike-targets-pulse')) {
const cycle = 2000
const phase = (elapsed % cycle) / cycle
const r = 35 * phase
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)
}
} catch (_) {}
animRef.current = requestAnimationFrame(tick)
}
const start = () => {
hideNonBelligerentLabels(map)
if (map.getLayer('attack-lines')) {
map.fitBounds(
[[THEATER_BBOX[0], THEATER_BBOX[1]], [THEATER_BBOX[2], THEATER_BBOX[3]]],
{ padding: 40, maxZoom: 5, duration: 0 }
)
const hasAnim =
(map.getSource('attack-dots') && attackPathsRef.current.length > 0) ||
(map.getSource('allied-strike-dots-lincoln') && lincolnPathsRef.current.length > 0) ||
(map.getSource('allied-strike-dots-ford') && fordPathsRef.current.length > 0) ||
(map.getSource('allied-strike-dots-israel') && israelPathsRef.current.length > 0)
if (hasAnim) {
animRef.current = requestAnimationFrame(tick)
} else {
animRef.current = requestAnimationFrame(start)
}
}
if (map.isStyleLoaded()) start()
else map.once('load', start)
start()
}
useEffect(() => {
return () => cancelAnimationFrame(animRef.current)
}, [])
@@ -215,6 +413,33 @@ export function WarMap() {
return (
<div className="relative h-full w-full">
{/* 图例 - 随容器自适应,避免遮挡 */}
<div className="absolute bottom-2 left-2 z-10 flex flex-wrap gap-x-3 gap-y-1 rounded bg-black/70 px-2 py-1.5 text-[9px] sm:text-[10px]">
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" />
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" />
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" />
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" />
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-sm bg-red-500/40" />
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" />
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-[#06B6D4]" />
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-[#22D3EE]" />
</span>
</div>
<Map
ref={mapRef}
initialViewState={DEFAULT_VIEW}
@@ -224,59 +449,60 @@ export function WarMap() {
dragRotate={false}
touchRotate={false}
style={{ width: '100%', height: '100%' }}
onLoad={(e) => {
// 地图加载完成后启动动画;延迟确保 Source/Layer 已挂载
setTimeout(() => initAnimation.current(e.target), 150)
}}
>
{/* 美国海军 - 蓝色 */}
{/* 矢量标记zoom 拉远变小,拉近变大 */}
<Source id="points-us-naval" type="geojson" data={usNaval}>
<Layer
id="points-us-naval"
type="circle"
paint={{
'circle-radius': 6,
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
'circle-color': '#3B82F6',
'circle-stroke-width': 2,
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
'circle-stroke-color': '#fff',
}}
/>
</Source>
{/* 美军基地-正常 - 绿色 */}
<Source id="points-us-base-op" type="geojson" data={usBaseOp}>
<Layer
id="points-us-base-op"
type="circle"
paint={{
'circle-radius': 6,
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
'circle-color': '#22C55E',
'circle-stroke-width': 2,
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
'circle-stroke-color': '#fff',
}}
/>
</Source>
{/* 美军基地-损毁 - 橙色闪烁 */}
<Source id="points-us-base-damaged" type="geojson" data={usBaseDamaged}>
<Layer
id="points-damaged"
type="circle"
paint={{
'circle-radius': 6,
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
'circle-color': '#F97316',
'circle-stroke-width': 2,
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
'circle-stroke-color': '#fff',
'circle-opacity': 1,
}}
/>
</Source>
{/* 美军基地-遭袭 - 红点 + 脉冲 */}
<Source id="points-us-base-attacked" type="geojson" data={usBaseAttacked}>
<Layer
id="points-attacked-dot"
type="circle"
paint={{
'circle-radius': 6,
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
'circle-color': '#EF4444',
'circle-stroke-width': 2,
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
'circle-stroke-color': '#fff',
}}
/>
@@ -284,14 +510,13 @@ export function WarMap() {
id="points-attacked-pulse"
type="circle"
paint={{
'circle-radius': 6,
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 4, 8, 12, 12, 24],
'circle-color': '#EF4444',
'circle-opacity': 0,
}}
/>
</Source>
{/* 伊朗 - 红色 */}
<Source
id="points-iran"
type="geojson"
@@ -304,54 +529,365 @@ export function WarMap() {
id="points-iran"
type="circle"
paint={{
'circle-radius': 6,
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
'circle-color': '#EF4444',
'circle-stroke-width': 2,
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
'circle-stroke-color': '#fff',
}}
/>
</Source>
{/* 美以联军打击伊朗:路径线 */}
<Source id="allied-strike-lines-lincoln" type="geojson" data={lincolnLinesGeoJson}>
<Layer
id="allied-strike-lines-lincoln"
type="line"
paint={{
'line-color': 'rgba(96, 165, 250, 0.45)',
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
}}
/>
</Source>
<Source id="allied-strike-lines-ford" type="geojson" data={fordLinesGeoJson}>
<Layer
id="allied-strike-lines-ford"
type="line"
paint={{
'line-color': 'rgba(6, 182, 212, 0.45)',
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
}}
/>
</Source>
<Source id="allied-strike-lines-israel" type="geojson" data={israelLinesGeoJson}>
<Layer
id="allied-strike-lines-israel"
type="line"
paint={{
'line-color': 'rgba(34, 211, 238, 0.45)',
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
}}
/>
</Source>
{/* 林肯号打击光点 */}
<Source
id="allied-strike-dots-lincoln"
type="geojson"
data={{
type: 'FeatureCollection',
features: lincolnPaths.map((path) => ({
type: 'Feature' as const,
properties: {},
geometry: { type: 'Point' as const, coordinates: path[0] },
})),
}}
>
<Layer
id="allied-strike-dots-lincoln-glow"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
'circle-color': 'rgba(96, 165, 250, 0.6)',
'circle-blur': 0.3,
}}
/>
<Layer
id="allied-strike-dots-lincoln-core"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
'circle-color': '#3B82F6',
'circle-stroke-width': 0.5,
'circle-stroke-color': '#fff',
}}
/>
</Source>
{/* 福特号打击光点 */}
<Source
id="allied-strike-dots-ford"
type="geojson"
data={{
type: 'FeatureCollection',
features: fordPaths.map((path) => ({
type: 'Feature' as const,
properties: {},
geometry: { type: 'Point' as const, coordinates: path[0] },
})),
}}
>
<Layer
id="allied-strike-dots-ford-glow"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
'circle-color': 'rgba(6, 182, 212, 0.6)',
'circle-blur': 0.3,
}}
/>
<Layer
id="allied-strike-dots-ford-core"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
'circle-color': '#06B6D4',
'circle-stroke-width': 0.5,
'circle-stroke-color': '#fff',
}}
/>
</Source>
{/* 以色列打击光点 */}
<Source
id="allied-strike-dots-israel"
type="geojson"
data={{
type: 'FeatureCollection',
features: israelPaths.map((path) => ({
type: 'Feature' as const,
properties: {},
geometry: { type: 'Point' as const, coordinates: path[0] },
})),
}}
>
<Layer
id="allied-strike-dots-israel-glow"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
'circle-color': 'rgba(34, 211, 238, 0.6)',
'circle-blur': 0.3,
}}
/>
<Layer
id="allied-strike-dots-israel-core"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
'circle-color': '#22D3EE',
'circle-stroke-width': 0.5,
'circle-stroke-color': '#fff',
}}
/>
</Source>
{/* 美军打击目标点位 (蓝色) */}
<Source
id="allied-strike-targets"
type="geojson"
data={{
type: 'FeatureCollection',
features: ALLIED_STRIKE_LOCATIONS.map((s) => ({
type: 'Feature' as const,
properties: { name: s.name },
geometry: { type: 'Point' as const, coordinates: s.coords },
})),
}}
>
<Layer
id="allied-strike-targets-circle"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 5, 2.5, 8, 4],
'circle-color': '#3B82F6',
'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1],
'circle-stroke-color': '#fff',
}}
/>
<Layer
id="allied-strike-targets-label"
type="symbol"
layout={{
'text-field': ['get', 'name'],
'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
'text-anchor': 'top',
'text-offset': [0, 0.8],
}}
paint={{
'text-color': '#60A5FA',
'text-halo-width': 0,
}}
/>
<Layer
id="allied-strike-targets-pulse"
type="circle"
paint={{
'circle-radius': 0,
'circle-color': 'rgba(96, 165, 250, 0.5)',
'circle-opacity': 0,
}}
/>
</Source>
{/* 伊朗攻击路径:细线 (矢量) */}
<Source id="attack-lines" type="geojson" data={attackLinesGeoJson}>
<Layer
id="attack-lines"
type="line"
paint={{
'line-color': '#ff0000',
'line-width': 2,
'line-dasharray': [2, 2],
'line-color': 'rgba(255, 100, 100, 0.4)',
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
}}
/>
</Source>
{/* 光点飞行动画:从德黑兰飞向各目标 */}
<Source
id="attack-dots"
type="geojson"
data={{
type: 'FeatureCollection',
features: attackPaths.map((path) => ({
type: 'Feature' as const,
properties: {},
geometry: {
type: 'Point' as const,
coordinates: path[0],
},
})),
}}
>
<Layer
id="attack-dots-glow"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
'circle-color': 'rgba(255, 100, 100, 0.6)',
'circle-blur': 0.3,
}}
/>
<Layer
id="attack-dots-core"
type="circle"
paint={{
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
'circle-color': '#ff4444',
'circle-stroke-width': 0.5,
'circle-stroke-color': '#fff',
}}
/>
</Source>
{/* 中文标注 */}
{/* 中文标注 - 随 zoom 自适应大小 */}
<Source id="labels" type="geojson" data={labelsGeoJson}>
<Layer
id="labels"
type="symbol"
layout={{
'text-field': ['get', 'name'],
'text-size': 11,
'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
'text-anchor': 'top',
'text-offset': [0, 0.8],
}}
paint={{
'text-color': '#E5E7EB',
'text-halo-color': 'rgba(0,0,0,0.8)',
'text-halo-width': 2,
'text-halo-width': 0,
}}
/>
</Source>
{/* 胡塞武装失控区 - 伊朗红 */}
<Source
id="houthi-area"
type="geojson"
data={{
type: 'Feature',
properties: { name: '胡塞武装' },
geometry: {
type: 'Polygon',
coordinates: [HOUTHI_POLYGON],
},
}}
>
<Layer
id="houthi-fill"
type="fill"
paint={{
'fill-color': 'rgba(239, 68, 68, 0.2)',
'fill-outline-color': '#EF4444',
'fill-opacity': 0.4,
}}
/>
</Source>
{/* 胡塞武装标注 */}
<Source
id="houthi-label"
type="geojson"
data={{
type: 'Feature',
properties: { name: '胡塞武装' },
geometry: { type: 'Point', coordinates: [44, 15.5] },
}}
>
<Layer
id="houthi-label-text"
type="symbol"
layout={{
'text-field': '胡塞武装',
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 9, 8, 12],
'text-anchor': 'center',
}}
paint={{
'text-color': '#EF4444',
'text-halo-width': 0,
}}
/>
</Source>
{/* 伊朗标注 */}
<Source
id="iran-label"
type="geojson"
data={{
type: 'Feature',
properties: { name: '伊朗' },
geometry: { type: 'Point', coordinates: [53.5, 32.5] },
}}
>
<Layer
id="iran-label-text"
type="symbol"
layout={{
'text-field': '伊朗',
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
'text-anchor': 'center',
}}
paint={{
'text-color': '#EF4444',
'text-halo-width': 0,
}}
/>
</Source>
{/* 以色列标注 */}
<Source
id="israel-label"
type="geojson"
data={{
type: 'Feature',
properties: { name: '以色列' },
geometry: { type: 'Point', coordinates: [35, 31] },
}}
>
<Layer
id="israel-label-text"
type="symbol"
layout={{
'text-field': '以色列',
'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
'text-anchor': 'center',
}}
paint={{
'text-color': '#60A5FA',
'text-halo-width': 0,
}}
/>
</Source>
<Source id="countries" type="geojson" data={COUNTRIES_GEOJSON}>
{/* 以色列 mesh 高亮 */}
{/* 伊朗区域填充 - 红色系 */}
<Layer
id="israel-fill"
id="iran-fill"
type="fill"
filter={['==', ['get', 'ADMIN'], 'Israel']}
filter={['==', ['get', 'ADMIN'], IRAN_ADMIN]}
paint={{
'fill-color': 'rgba(96, 165, 250, 0.25)',
'fill-outline-color': '#60A5FA',
'fill-color': 'rgba(239, 68, 68, 0.15)',
'fill-outline-color': '#EF4444',
'fill-opacity': 0.5,
}}
/>
<Layer
@@ -364,6 +900,17 @@ export function WarMap() {
'line-opacity': 0.9,
}}
/>
{/* 以色列区域填充 - 蓝色系 */}
<Layer
id="israel-fill"
type="fill"
filter={['==', ['get', 'ADMIN'], 'Israel']}
paint={{
'fill-color': 'rgba(96, 165, 250, 0.25)',
'fill-outline-color': '#60A5FA',
'fill-opacity': 0.6,
}}
/>
<Layer
id="allies-outline"
type="line"