+
+
![]()
+
@@ -100,6 +109,10 @@ const chartRef = ref(null)
const sourceChartRef = ref(null)
const salesChartRef = ref(null)
const currentMode = ref('outflow')
+const isMapContentSwitching = ref(false)
+const mapStageLocal = computed(() => (
+ currentMode.value === 'local' && !isMapContentSwitching.value
+))
let chartInstance = null
let sourceChartInstance = null
let salesChartInstance = null
@@ -113,6 +126,7 @@ let unitedMapKey = ''
const mapBgImageCache = new Map()
const mapBgImageLoading = new Map()
let mapSurfaceTexture = null
+let mapSurfaceTextureDataUrl = ''
let mapSurfaceTextureKey = ''
// 浮动面板状态
@@ -150,26 +164,27 @@ const MAP_SHADOW_OFFSET_FAR = 1.85
const MAP_SHADOW_OFFSET_MID = 1.2
const MAP_SHADOW_OFFSET_NEAR = 0.62
const MAP_SHADOW_OFFSET_LNG = -0.42
-const MAP_SHADOW_OFFSET_FAR_LOCAL = 5
-const MAP_SHADOW_OFFSET_MID_LOCAL = 3.5
-const MAP_SHADOW_OFFSET_NEAR_LOCAL = 2.2
-const MAP_SHADOW_OFFSET_LNG_LOCAL = -2
+const MAP_SHADOW_OFFSET_FAR_LOCAL = 2.6
+const MAP_SHADOW_OFFSET_MID_LOCAL = 1.8
+const MAP_SHADOW_OFFSET_NEAR_LOCAL = 1.0
+const MAP_SHADOW_OFFSET_LNG_LOCAL = -1.5
const MAP_WIREFRAME_OFFSET = -0.72
const MAP_WIREFRAME_OFFSET_LNG = 0.1
-const MAP_WIREFRAME_OFFSET_LOCAL = -3.5
+const MAP_WIREFRAME_OFFSET_LOCAL = -1.8
const MAP_WIREFRAME_OFFSET_LNG_LOCAL = 0.6
-// 国界描边:略偏青蓝(非纯白),光晕同色渐弱
-const MAP_WIREFRAME_COLOR = '#C8E8F4'
+// 国界描边:青蓝霓虹光晕(对齐设计稿,避免过白过曝)
+const MAP_WIREFRAME_COLOR = '#B8ECF6'
const MAP_WIREFRAME_HALO_LAYERS = [
- { width: 12, color: 'rgba(148, 208, 228, 0.08)' },
- { width: 8.5, color: 'rgba(162, 220, 238, 0.15)' },
- { width: 5.8, color: 'rgba(176, 232, 246, 0.25)' }
+ { width: 10, color: 'rgba(128, 208, 228, 0.06)' },
+ { width: 7, color: 'rgba(148, 218, 236, 0.12)' },
+ { width: 4.6, color: 'rgba(168, 232, 246, 0.22)' }
]
-const MAP_WIREFRAME_CORE_WIDTH = 3.1
-const MAP_WIREFRAME_CORE_SHADOW_BLUR = 8
-const MAP_WIREFRAME_CORE_SHADOW_COLOR = 'rgba(120, 198, 228, 0.24)'
-const MAP_INNER_BORDER = 'rgba(4, 32, 52, 0.28)'
-const MAP_INNER_BORDER_WIDTH = 0.45
+const MAP_WIREFRAME_CORE_WIDTH = 2.6
+const MAP_WIREFRAME_CORE_SHADOW_BLUR = 7
+const MAP_WIREFRAME_CORE_SHADOW_COLOR = 'rgba(110, 200, 228, 0.22)'
+const MAP_INNER_BORDER = 'rgba(1, 12, 22, 0.82)'
+const MAP_INNER_BORDER_WIDTH = 0.95
+const MAP_INNER_BORDER_WIDTH_LOCAL = 1.2
const MAP_SHADOW_FILL_FAR = '#010204'
const MAP_SHADOW_FILL_MID = '#020408'
const MAP_SHADOW_FILL_NEAR = '#030a12'
@@ -183,8 +198,141 @@ const MAP_BG_CROP_CHINA = { x: 1160, y: 108, width: 2000, height: 828 }
const MAP_BG_CROP_LOCAL = { x: 1860, y: 332, width: 820, height: 520 }
const MAP_SURFACE_TEXTURE_WIDTH = 1536
const MAP_SURFACE_TEXTURE_HEIGHT = 1152
-const MAP_SURFACE_FALLBACK = 'rgb(92, 202, 206)'
+const MAP_SURFACE_FALLBACK = 'rgb(49, 183, 183)'
const MAP_SURFACE_COLOR = 'rgba(0, 0, 0, 0)'
+const MAP_SURFACE_PAINT_VERSION = 5
+
+// 全国:左亮青 → 右深蓝,地形纹路清晰(color 定色相 + 轻 overlay 提饱和)
+const WORLD_SURFACE_PAINT = {
+ brightness: 1.22,
+ contrast: 1.26,
+ lift: {
+ enabled: true,
+ composite: 'screen',
+ color: 'rgba(220, 248, 244, 0.16)'
+ },
+ tint: {
+ composite: 'color',
+ mode: 'linear',
+ linearX0: 0,
+ linearY0: 0.5,
+ linearX1: 1,
+ linearY1: 0.5,
+ stops: [
+ { stop: 0, color: 'rgb(85, 212, 205)' },
+ { stop: 0.45, color: 'rgb(52, 175, 188)' },
+ { stop: 1, color: 'rgb(14, 131, 184)' }
+ ]
+ },
+ satBoost: {
+ enabled: true,
+ composite: 'overlay',
+ mode: 'linear',
+ linearX0: 0,
+ linearY0: 0.5,
+ linearX1: 1,
+ linearY1: 0.5,
+ stops: [
+ { stop: 0, color: 'rgba(145, 245, 236, 0.22)' },
+ { stop: 0.5, color: 'rgba(80, 205, 208, 0.12)' },
+ { stop: 1, color: 'rgba(18, 135, 180, 0.16)' }
+ ]
+ },
+ depth: { enabled: false },
+ softLight: { enabled: true, color: 'rgba(125, 222, 215, 0.09)' },
+ glow: {
+ enabled: true,
+ centerX: 0.34,
+ centerY: 0.44,
+ radius: 0.64,
+ stops: [
+ { stop: 0, color: 'rgba(228, 252, 248, 0.22)' },
+ { stop: 0.48, color: 'rgba(185, 238, 232, 0.08)' },
+ { stop: 1, color: 'rgba(0, 0, 0, 0)' }
+ ]
+ }
+}
+
+// 红原:中西部亮、边缘深蓝(对齐设计稿径向)
+const LOCAL_SURFACE_PAINT = {
+ brightness: 1.22,
+ contrast: 1.28,
+ lift: {
+ enabled: true,
+ composite: 'screen',
+ color: 'rgba(232, 252, 248, 0.18)'
+ },
+ tint: {
+ composite: 'color',
+ mode: 'radial',
+ centerX: 0.3,
+ centerY: 0.47,
+ radius: 0.78,
+ stops: [
+ { stop: 0, color: 'rgb(188, 246, 238)' },
+ { stop: 0.28, color: 'rgb(82, 200, 205)' },
+ { stop: 0.62, color: 'rgb(35, 150, 178)' },
+ { stop: 1, color: 'rgb(14, 131, 184)' }
+ ]
+ },
+ satBoost: {
+ enabled: true,
+ composite: 'overlay',
+ mode: 'radial',
+ centerX: 0.3,
+ centerY: 0.47,
+ radius: 0.74,
+ stops: [
+ { stop: 0, color: 'rgba(225, 252, 246, 0.34)' },
+ { stop: 0.35, color: 'rgba(98, 218, 212, 0.17)' },
+ { stop: 1, color: 'rgba(14, 131, 184, 0.22)' }
+ ]
+ },
+ depth: { enabled: false },
+ softLight: { enabled: true, color: 'rgba(112, 220, 212, 0.1)' },
+ glow: {
+ enabled: true,
+ centerX: 0.28,
+ centerY: 0.46,
+ radius: 0.5,
+ stops: [
+ { stop: 0, color: 'rgba(255, 255, 253, 0.36)' },
+ { stop: 0.5, color: 'rgba(205, 242, 236, 0.11)' },
+ { stop: 1, color: 'rgba(0, 0, 0, 0)' }
+ ]
+ }
+}
+
+const getMapSurfacePaintConfig = () => (
+ currentMode.value === 'local' ? LOCAL_SURFACE_PAINT : WORLD_SURFACE_PAINT
+)
+
+const createSurfacePaintGradient = (ctx, width, height, layer) => {
+ if (layer.mode === 'linear') {
+ return ctx.createLinearGradient(
+ width * layer.linearX0,
+ height * layer.linearY0,
+ width * layer.linearX1,
+ height * layer.linearY1
+ )
+ }
+ return ctx.createRadialGradient(
+ width * layer.centerX,
+ height * layer.centerY,
+ 0,
+ width * layer.centerX,
+ height * layer.centerY,
+ Math.max(width, height) * layer.radius
+ )
+}
+
+const applySurfacePaintLayer = (ctx, width, height, layer) => {
+ const gradient = createSurfacePaintGradient(ctx, width, height, layer)
+ layer.stops.forEach(({ stop, color }) => gradient.addColorStop(stop, color))
+ ctx.globalCompositeOperation = layer.composite
+ ctx.fillStyle = gradient
+ ctx.fillRect(0, 0, width, height)
+}
const getMapBgImageSrc = (mode = currentMode.value) => (
mode === 'local' ? MAP_BG_IMAGE_LOCAL : MAP_BG_IMAGE_WORLD
@@ -783,10 +931,11 @@ const loadMapBgImage = (mode = currentMode.value) => {
}
const paintMapSurfaceTexture = (ctx, width, height, image, crop) => {
+ const paint = getMapSurfacePaintConfig()
ctx.clearRect(0, 0, width, height)
- // 1. 地形纹理打底(略提亮对比,保证纹路可见)
- ctx.filter = 'brightness(1.28) contrast(1.18)'
+ // 1. 地形底图:保留山脉/河谷明暗,勿过度提亮
+ ctx.filter = `brightness(${paint.brightness}) contrast(${paint.contrast})`
ctx.drawImage(
image,
crop.x,
@@ -800,43 +949,46 @@ const paintMapSurfaceTexture = (ctx, width, height, image, crop) => {
)
ctx.filter = 'none'
- // 2. overlay 青蓝染色:G/B 均衡,介于青色与蓝色之间
- ctx.globalCompositeOperation = 'overlay'
- const tintGradient = ctx.createRadialGradient(
- width * 0.44,
- height * 0.48,
- 0,
- width * 0.44,
- height * 0.48,
- Math.max(width, height) * 0.86
- )
- tintGradient.addColorStop(0, 'rgba(158, 236, 228, 0.94)')
- tintGradient.addColorStop(0.42, 'rgba(108, 212, 214, 0.90)')
- tintGradient.addColorStop(0.78, 'rgba(82, 194, 200, 0.86)')
- tintGradient.addColorStop(1, 'rgba(68, 180, 188, 0.82)')
- ctx.fillStyle = tintGradient
- ctx.fillRect(0, 0, width, height)
+ // 1.5 先整体提亮暗部(避免 color 混合把地形压得太暗)
+ if (paint.lift?.enabled) {
+ ctx.globalCompositeOperation = paint.lift.composite || 'screen'
+ ctx.fillStyle = paint.lift.color
+ ctx.fillRect(0, 0, width, height)
+ }
- // 3. soft-light 补一层青蓝,提亮暗部
- ctx.globalCompositeOperation = 'soft-light'
- ctx.fillStyle = 'rgba(118, 218, 214, 0.36)'
- ctx.fillRect(0, 0, width, height)
+ // 2. color 定色相,保留地形明暗
+ applySurfacePaintLayer(ctx, width, height, paint.tint)
- // 4. screen 中心高光
- ctx.globalCompositeOperation = 'screen'
- const glowGradient = ctx.createRadialGradient(
- width * 0.43,
- height * 0.46,
- 0,
- width * 0.43,
- height * 0.46,
- Math.max(width, height) * 0.58
- )
- glowGradient.addColorStop(0, 'rgba(205, 248, 242, 0.34)')
- glowGradient.addColorStop(0.55, 'rgba(175, 235, 228, 0.14)')
- glowGradient.addColorStop(1, 'rgba(175, 235, 228, 0)')
- ctx.fillStyle = glowGradient
- ctx.fillRect(0, 0, width, height)
+ // 2.5 轻量 overlay 提亮饱和(避免单层 overlay 洗成一片亮蓝)
+ if (paint.satBoost?.enabled) {
+ applySurfacePaintLayer(ctx, width, height, paint.satBoost)
+ }
+
+ // 3. 东侧/边缘略压暗,增加左右纵深
+ if (paint.depth?.enabled) {
+ applySurfacePaintLayer(ctx, width, height, paint.depth)
+ }
+
+ // 4. 轻量 soft-light 提亮暗部
+ if (paint.softLight?.enabled) {
+ ctx.globalCompositeOperation = 'soft-light'
+ ctx.fillStyle = paint.softLight.color
+ ctx.fillRect(0, 0, width, height)
+ }
+
+ // 5. 中心柔和高光
+ if (paint.glow?.enabled) {
+ ctx.globalCompositeOperation = 'screen'
+ const glowGradient = createSurfacePaintGradient(ctx, width, height, {
+ mode: 'radial',
+ centerX: paint.glow.centerX,
+ centerY: paint.glow.centerY,
+ radius: paint.glow.radius
+ })
+ paint.glow.stops.forEach(({ stop, color }) => glowGradient.addColorStop(stop, color))
+ ctx.fillStyle = glowGradient
+ ctx.fillRect(0, 0, width, height)
+ }
ctx.globalCompositeOperation = 'source-over'
}
@@ -856,23 +1008,39 @@ const createMapSurfaceTexture = (image) => {
return canvas
}
-const ensureMapSurfaceTexture = async () => {
- const key = currentMode.value
- if (mapSurfaceTexture && mapSurfaceTextureKey === key) {
+const invalidateMapSurfaceTexture = () => {
+ mapSurfaceTexture = null
+ mapSurfaceTextureDataUrl = ''
+ mapSurfaceTextureKey = ''
+}
+
+const syncMapSurfaceTextureOutput = (canvas) => {
+ mapSurfaceTexture = canvas
+ mapSurfaceTextureDataUrl = canvas ? canvas.toDataURL('image/png') : ''
+ return mapSurfaceTexture
+}
+
+const ensureMapSurfaceTexture = async (force = false) => {
+ const key = `${currentMode.value}@v${MAP_SURFACE_PAINT_VERSION}`
+ if (!force && mapSurfaceTexture && mapSurfaceTextureKey === key) {
return mapSurfaceTexture
}
const image = await loadMapBgImage(key)
- mapSurfaceTexture = createMapSurfaceTexture(image)
+ const canvas = createMapSurfaceTexture(image)
+ if (!canvas) {
+ return null
+ }
+ syncMapSurfaceTextureOutput(canvas)
mapSurfaceTextureKey = key
return mapSurfaceTexture
}
const getMapSurfaceAreaStyle = () => {
- if (mapSurfaceTexture) {
+ if (mapSurfaceTextureDataUrl) {
return {
areaColor: {
- image: mapSurfaceTexture,
+ image: mapSurfaceTextureDataUrl,
repeat: 'no-repeat'
}
}
@@ -887,7 +1055,8 @@ const refreshMapSurfaceTexture = async () => {
}
try {
- await ensureMapSurfaceTexture()
+ invalidateMapSurfaceTexture()
+ await ensureMapSurfaceTexture(true)
const mapBounds = calculateMapBounds()
const mapName = getMapName(currentMode.value)
const unitedMapName = registerUnitedMap(currentMode.value)
@@ -1252,7 +1421,9 @@ const getChartOption = () => {
itemStyle: {
areaColor: MAP_SURFACE_COLOR,
borderColor: MAP_INNER_BORDER,
- borderWidth: MAP_INNER_BORDER_WIDTH
+ borderWidth: currentMode.value === 'local'
+ ? MAP_INNER_BORDER_WIDTH_LOCAL
+ : MAP_INNER_BORDER_WIDTH
},
label: {
show: true,
@@ -1496,19 +1667,27 @@ const updateChart = async (needReloadMap = false) => {
}
}
-// 监听模式变化
-watch(currentMode, (newMode, oldMode) => {
+// 监听模式变化:跨地图切换时先隐藏,避免红原扶正与中国地图替换分两段播放
+watch(currentMode, async (newMode, oldMode) => {
emit('mode-change', newMode)
- nextTick(async () => {
- // 判断是否需要重新加载地图数据
- const oldMapName = getMapName(oldMode)
- const newMapName = getMapName(newMode)
- const needReloadMap = oldMapName !== newMapName
-
- await updateChart(needReloadMap)
- setTimeout(handleChartResize, 580)
- })
-}, { immediate: true })
+
+ if (oldMode === undefined) {
+ return
+ }
+
+ const needReloadMap = getMapName(oldMode) !== getMapName(newMode)
+ if (needReloadMap) {
+ isMapContentSwitching.value = true
+ }
+
+ await updateChart(needReloadMap)
+ await nextTick()
+ isMapContentSwitching.value = false
+ handleChartResize()
+ if (needReloadMap) {
+ requestAnimationFrame(() => handleChartResize())
+ }
+}, { flush: 'pre', immediate: true })
onMounted(() => {
initChart()
@@ -1636,28 +1815,28 @@ onUnmounted(() => {
inset: 0;
transform-style: preserve-3d;
transform-origin: 50% 55%;
- transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
- will-change: transform;
+ will-change: transform, opacity;
backface-visibility: hidden;
}
+.map-tilt-layer--switching {
+ opacity: 0;
+ pointer-events: none;
+}
+
.map-stage--local .map-tilt-layer {
- transform: rotateX(22deg) translate3d(0, -6%, 22px) scale(0.92);
+ transform: rotateX(33deg) translate3d(0, -9%, 22px) scale(0.88);
+ transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.map-stage--local .chart-container {
transform: none;
}
-.map-tilt-layer .map-world-bg,
.map-tilt-layer .chart-container {
transform-style: preserve-3d;
}
-.map-stage--local .map-world-bg {
- transform: translate3d(-50%, -50%, 0);
-}
-
.map-world-bg {
position: absolute;
width: var(--screen-width, 5120px);