Refactor ChinaMap and Dashboard components to streamline mode handling and background image management; enhance button layout in ChinaMap for improved user interaction; update computed properties for dynamic background image sourcing based on selected mode.

main
Swanky 3 days ago
parent 2954dc2e92
commit c9503210c5
  1. BIN
      public/images/红原背景.png
  2. 198
      src/components/ChinaMap.vue
  3. 13
      src/views/Dashboard.vue

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

@ -3,47 +3,28 @@
<!-- 顶部切换按钮 --> <!-- 顶部切换按钮 -->
<div class="map-controls"> <div class="map-controls">
<div class="control-buttons"> <div class="control-buttons">
<button <button v-for="mode in modes" :key="mode.key" type="button" class="mode-btn"
v-for="mode in modes" :class="{ active: currentMode === mode.key }" @click="currentMode = mode.key">
:key="mode.key" <img class="mode-btn-bg" :src="currentMode === mode.key ? BTN_ACTIVE : BTN_NORMAL" alt="" />
type="button"
class="mode-btn"
:class="{ active: currentMode === mode.key }"
@click="currentMode = mode.key"
>
<img
class="mode-btn-bg"
:src="currentMode === mode.key ? BTN_ACTIVE : BTN_NORMAL"
alt=""
/>
<span class="mode-btn-text">{{ mode.label }}</span> <span class="mode-btn-text">{{ mode.label }}</span>
</button> </button>
</div> </div>
</div> </div>
<!-- 左侧来源分析面板 --> <!-- 左侧来源分析面板 -->
<div <div v-if="showSourcePanel && sourceData" class="source-panel floating-panel">
v-if="showSourcePanel && sourceData"
class="source-panel floating-panel"
>
<h3>牦牛来源分析</h3> <h3>牦牛来源分析</h3>
<div ref="sourceChartRef" class="panel-chart"></div> <div ref="sourceChartRef" class="panel-chart"></div>
</div> </div>
<!-- 右侧销售分析面板 --> <!-- 右侧销售分析面板 -->
<div <div v-if="showSalesPanel && salesData" class="sales-panel floating-panel">
v-if="showSalesPanel && salesData"
class="sales-panel floating-panel"
>
<h3>牦牛销售分析</h3> <h3>牦牛销售分析</h3>
<div ref="salesChartRef" class="panel-chart"></div> <div ref="salesChartRef" class="panel-chart"></div>
</div> </div>
<!-- 交易明细浮动框 --> <!-- 交易明细浮动框 -->
<div <div v-if="showDetailPanel && detailData" class="detail-panel">
v-if="showDetailPanel && detailData"
class="detail-panel"
>
<div class="detail-header"> <div class="detail-header">
<h3>{{ selectedCity }} - 交易明细统计</h3> <h3>{{ selectedCity }} - 交易明细统计</h3>
<button @click="closeDetailPanel" class="close-btn">×</button> <button @click="closeDetailPanel" class="close-btn">×</button>
@ -78,13 +59,11 @@
</div> </div>
</div> </div>
<div class="map-stage"> <div class="map-stage" :class="{ 'map-stage--local': currentMode === 'local' }">
image.png <img <div class="map-tilt-layer">
class="map-world-bg" <img class="map-world-bg" :src="mapWorldBgSrc" alt="" />
src="/images/世界地图背景.png"
alt=""
/>
<div ref="chartRef" class="chart-container"></div> <div ref="chartRef" class="chart-container"></div>
</div>
<div class="map-legend" aria-hidden="true"> <div class="map-legend" aria-hidden="true">
<img class="map-legend-bg" :src="LEGEND_BG" alt="" /> <img class="map-legend-bg" :src="LEGEND_BG" alt="" />
<ul class="map-legend-list"> <ul class="map-legend-list">
@ -107,7 +86,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue' import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import BaseCard from './BaseCard.vue' import BaseCard from './BaseCard.vue'
import { loadSystemConfig, DEFAULT_MAP_HUB, DEFAULT_MAP_FLOW_LEVELS } from '../utils/systemConfig.js' import { loadSystemConfig, DEFAULT_MAP_HUB, DEFAULT_MAP_FLOW_LEVELS } from '../utils/systemConfig.js'
@ -115,6 +94,8 @@ import { loadSystemConfig, DEFAULT_MAP_HUB, DEFAULT_MAP_FLOW_LEVELS } from '../u
const MAP_TRADING_API = '/api/dashboard/map-trading-network' const MAP_TRADING_API = '/api/dashboard/map-trading-network'
const FALLBACK_TRADING_JSON = './yak-trading-data.json' const FALLBACK_TRADING_JSON = './yak-trading-data.json'
const emit = defineEmits(['mode-change'])
const chartRef = ref(null) const chartRef = ref(null)
const sourceChartRef = ref(null) const sourceChartRef = ref(null)
const salesChartRef = ref(null) const salesChartRef = ref(null)
@ -129,8 +110,8 @@ let mapSystemConfig = null
let cachedOutlineKey = '' let cachedOutlineKey = ''
let cachedOutlinePaths = [] let cachedOutlinePaths = []
let unitedMapKey = '' let unitedMapKey = ''
let mapBgImage = null const mapBgImageCache = new Map()
let mapBgImagePromise = null const mapBgImageLoading = new Map()
let mapSurfaceTexture = null let mapSurfaceTexture = null
let mapSurfaceTextureKey = '' let mapSurfaceTextureKey = ''
@ -177,15 +158,25 @@ const MAP_WIREFRAME_OFFSET = -0.72
const MAP_WIREFRAME_OFFSET_LNG = 0.1 const MAP_WIREFRAME_OFFSET_LNG = 0.1
const MAP_WIREFRAME_OFFSET_LOCAL = -3.5 const MAP_WIREFRAME_OFFSET_LOCAL = -3.5
const MAP_WIREFRAME_OFFSET_LNG_LOCAL = 0.6 const MAP_WIREFRAME_OFFSET_LNG_LOCAL = 0.6
const MAP_WIREFRAME_COLOR = '#f2fdff' //
const MAP_WIREFRAME_COLOR = '#C8E8F4'
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)' }
]
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 = 'rgba(4, 32, 52, 0.28)'
const MAP_INNER_BORDER_WIDTH = 0.45 const MAP_INNER_BORDER_WIDTH = 0.45
const MAP_SHADOW_FILL_FAR = '#010204' const MAP_SHADOW_FILL_FAR = '#010204'
const MAP_SHADOW_FILL_MID = '#020408' const MAP_SHADOW_FILL_MID = '#020408'
const MAP_SHADOW_FILL_NEAR = '#030a12' const MAP_SHADOW_FILL_NEAR = '#030a12'
const MAP_LABEL_COLOR = '#ffffff' const MAP_LABEL_COLOR = '#ffffff'
// 4318×1078 // /
const MAP_BG_IMAGE = '/images/世界地图背景.png' const MAP_BG_IMAGE_WORLD = '/images/世界地图背景.png'
const MAP_BG_IMAGE_LOCAL = '/images/红原背景.png'
const MAP_BG_NATIVE_WIDTH = 4318 const MAP_BG_NATIVE_WIDTH = 4318
const MAP_BG_NATIVE_HEIGHT = 1078 const MAP_BG_NATIVE_HEIGHT = 1078
const MAP_BG_CROP_CHINA = { x: 1160, y: 108, width: 2000, height: 828 } const MAP_BG_CROP_CHINA = { x: 1160, y: 108, width: 2000, height: 828 }
@ -195,6 +186,12 @@ const MAP_SURFACE_TEXTURE_HEIGHT = 1152
const MAP_SURFACE_FALLBACK = 'rgb(92, 202, 206)' const MAP_SURFACE_FALLBACK = 'rgb(92, 202, 206)'
const MAP_SURFACE_COLOR = 'rgba(0, 0, 0, 0)' const MAP_SURFACE_COLOR = 'rgba(0, 0, 0, 0)'
const getMapBgImageSrc = (mode = currentMode.value) => (
mode === 'local' ? MAP_BG_IMAGE_LOCAL : MAP_BG_IMAGE_WORLD
)
const mapWorldBgSrc = computed(() => getMapBgImageSrc(currentMode.value))
// path ECharts 沿线 // path ECharts 沿线
const METEOR_EFFECT_PATH = 'path://M0.5,0 L0.56,0.14 L0.52,1 L0.48,1 L0.44,0.14 Z' const METEOR_EFFECT_PATH = 'path://M0.5,0 L0.56,0.14 L0.52,1 L0.48,1 L0.44,0.14 Z'
@ -742,19 +739,26 @@ const registerUnitedMap = (mapMode = currentMode.value) => {
return unitedName return unitedName
} }
const getMapBgCrop = () => ( const getMapBgCrop = (image) => {
currentMode.value === 'local' ? MAP_BG_CROP_LOCAL : MAP_BG_CROP_CHINA if (currentMode.value === 'local') {
) if (image?.naturalWidth && image?.naturalHeight) {
return { x: 0, y: 0, width: image.naturalWidth, height: image.naturalHeight }
}
return MAP_BG_CROP_LOCAL
}
return MAP_BG_CROP_CHINA
}
const loadMapBgImage = () => { const loadMapBgImage = (mode = currentMode.value) => {
if (mapBgImage) { const src = getMapBgImageSrc(mode)
return Promise.resolve(mapBgImage) if (mapBgImageCache.has(src)) {
return Promise.resolve(mapBgImageCache.get(src))
} }
if (mapBgImagePromise) { if (mapBgImageLoading.has(src)) {
return mapBgImagePromise return mapBgImageLoading.get(src)
} }
mapBgImagePromise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
if (typeof Image === 'undefined') { if (typeof Image === 'undefined') {
reject(new Error('Image is not available')) reject(new Error('Image is not available'))
return return
@ -763,17 +767,19 @@ const loadMapBgImage = () => {
const image = new Image() const image = new Image()
image.decoding = 'async' image.decoding = 'async'
image.onload = () => { image.onload = () => {
mapBgImage = image mapBgImageCache.set(src, image)
mapBgImageLoading.delete(src)
resolve(image) resolve(image)
} }
image.onerror = () => { image.onerror = () => {
mapBgImagePromise = null mapBgImageLoading.delete(src)
reject(new Error(`Failed to load ${MAP_BG_IMAGE}`)) reject(new Error(`Failed to load ${src}`))
} }
image.src = MAP_BG_IMAGE image.src = src
}) })
return mapBgImagePromise mapBgImageLoading.set(src, promise)
return promise
} }
const paintMapSurfaceTexture = (ctx, width, height, image, crop) => { const paintMapSurfaceTexture = (ctx, width, height, image, crop) => {
@ -846,7 +852,7 @@ const createMapSurfaceTexture = (image) => {
return null return null
} }
paintMapSurfaceTexture(ctx, width, height, image, getMapBgCrop()) paintMapSurfaceTexture(ctx, width, height, image, getMapBgCrop(image))
return canvas return canvas
} }
@ -856,7 +862,7 @@ const ensureMapSurfaceTexture = async () => {
return mapSurfaceTexture return mapSurfaceTexture
} }
const image = await loadMapBgImage() const image = await loadMapBgImage(key)
mapSurfaceTexture = createMapSurfaceTexture(image) mapSurfaceTexture = createMapSurfaceTexture(image)
mapSurfaceTextureKey = key mapSurfaceTextureKey = key
return mapSurfaceTexture return mapSurfaceTexture
@ -1139,6 +1145,24 @@ const buildGeoLayers = (mapName, unitedMapName, mapBounds) => {
] ]
} }
const buildWireframeLineSeries = (name, zlevel, wireframeData, lineStyle) => ({
name,
type: 'lines',
coordinateSystem: 'geo',
geoIndex: WIREFRAME_GEO_INDEX,
zlevel,
polyline: true,
silent: true,
data: wireframeData,
lineStyle: {
cap: 'round',
join: 'round',
shadowOffsetX: 0,
shadowOffsetY: 0,
...lineStyle
}
})
// //
const getChartOption = () => { const getChartOption = () => {
const modeData = getCurrentModeData() const modeData = getCurrentModeData()
@ -1243,25 +1267,23 @@ const getChartOption = () => {
disabled: true disabled: true
} }
}, },
...MAP_WIREFRAME_HALO_LAYERS.map((layer, index) => buildWireframeLineSeries(
`mapWireframeHalo${index}`,
4,
wireframeData,
{ {
name: 'mapWireframe', color: layer.color,
type: 'lines', width: layer.width,
coordinateSystem: 'geo', opacity: 1
geoIndex: WIREFRAME_GEO_INDEX,
zlevel: 5,
polyline: true,
silent: true,
data: wireframeData,
lineStyle: {
color: MAP_WIREFRAME_COLOR,
width: 4.2,
opacity: 0.98,
cap: 'round',
join: 'round',
shadowBlur: 22,
shadowColor: 'rgba(130, 245, 255, 0.78)'
} }
}, )),
buildWireframeLineSeries('mapWireframe', 5, wireframeData, {
color: MAP_WIREFRAME_COLOR,
width: MAP_WIREFRAME_CORE_WIDTH,
opacity: 0.93,
shadowBlur: MAP_WIREFRAME_CORE_SHADOW_BLUR,
shadowColor: MAP_WIREFRAME_CORE_SHADOW_COLOR
}),
{ {
name: '交易城市', name: '交易城市',
type: 'scatter', type: 'scatter',
@ -1476,6 +1498,7 @@ const updateChart = async (needReloadMap = false) => {
// //
watch(currentMode, (newMode, oldMode) => { watch(currentMode, (newMode, oldMode) => {
emit('mode-change', newMode)
nextTick(async () => { nextTick(async () => {
// //
const oldMapName = getMapName(oldMode) const oldMapName = getMapName(oldMode)
@ -1483,8 +1506,9 @@ watch(currentMode, (newMode, oldMode) => {
const needReloadMap = oldMapName !== newMapName const needReloadMap = oldMapName !== newMapName
await updateChart(needReloadMap) await updateChart(needReloadMap)
setTimeout(handleChartResize, 580)
}) })
}) }, { immediate: true })
onMounted(() => { onMounted(() => {
initChart() initChart()
@ -1602,6 +1626,38 @@ onUnmounted(() => {
background: transparent; background: transparent;
} }
.map-stage--local {
perspective: 900px;
perspective-origin: 50% 58%;
}
.map-tilt-layer {
position: absolute;
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;
backface-visibility: hidden;
}
.map-stage--local .map-tilt-layer {
transform: rotateX(22deg) translate3d(0, -6%, 22px) scale(0.92);
}
.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 { .map-world-bg {
position: absolute; position: absolute;
width: var(--screen-width, 5120px); width: var(--screen-width, 5120px);

@ -2,7 +2,7 @@
<div class="dashboard-container"> <div class="dashboard-container">
<img <img
class="dashboard-bg" class="dashboard-bg"
src="/images/世界地图背景.png" :src="dashboardBgSrc"
alt="" alt=""
/> />
@ -44,7 +44,7 @@
<!-- 中央地图 --> <!-- 中央地图 -->
<div class="center-section"> <div class="center-section">
<div class="map-container"> <div class="map-container">
<ChinaMap /> <ChinaMap @mode-change="mapCurrentMode = $event" />
</div> </div>
</div> </div>
@ -105,7 +105,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, computed, onMounted, nextTick } from 'vue'
import { configManager } from '../utils/config.js' import { configManager } from '../utils/config.js'
import { loadSystemConfig } from '../utils/systemConfig.js' import { loadSystemConfig } from '../utils/systemConfig.js'
import { dataManager } from '../utils/dataManager.js' import { dataManager } from '../utils/dataManager.js'
@ -138,6 +138,13 @@ const isLayoutReady = ref(false) // 控制页面渲染时机
const isSupplyExpanded = ref(false) // const isSupplyExpanded = ref(false) //
const supplyDetailId = ref('') const supplyDetailId = ref('')
const DASHBOARD_BG_WORLD = '/images/世界地图背景.png'
const DASHBOARD_BG_LOCAL = '/images/红原背景.png'
const mapCurrentMode = ref('outflow')
const dashboardBgSrc = computed(() => (
mapCurrentMode.value === 'local' ? DASHBOARD_BG_LOCAL : DASHBOARD_BG_WORLD
))
// / // /
const handleSupplyExpand = (payload) => { const handleSupplyExpand = (payload) => {
if (typeof payload === 'boolean') { if (typeof payload === 'boolean') {

Loading…
Cancel
Save