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. 288
      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="control-buttons">
<button
v-for="mode in modes"
:key="mode.key"
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=""
/>
<button v-for="mode in modes" :key="mode.key" 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>
</button>
</div>
</div>
<!-- 左侧来源分析面板 -->
<div
v-if="showSourcePanel && sourceData"
class="source-panel floating-panel"
>
<div v-if="showSourcePanel && sourceData" class="source-panel floating-panel">
<h3>牦牛来源分析</h3>
<div ref="sourceChartRef" class="panel-chart"></div>
</div>
<!-- 右侧销售分析面板 -->
<div
v-if="showSalesPanel && salesData"
class="sales-panel floating-panel"
>
<div v-if="showSalesPanel && salesData" class="sales-panel floating-panel">
<h3>牦牛销售分析</h3>
<div ref="salesChartRef" class="panel-chart"></div>
</div>
<!-- 交易明细浮动框 -->
<div
v-if="showDetailPanel && detailData"
class="detail-panel"
>
<div v-if="showDetailPanel && detailData" class="detail-panel">
<div class="detail-header">
<h3>{{ selectedCity }} - 交易明细统计</h3>
<button @click="closeDetailPanel" class="close-btn">×</button>
@ -77,14 +58,12 @@
</table>
</div>
</div>
<div class="map-stage">
image.png <img
class="map-world-bg"
src="/images/世界地图背景.png"
alt=""
/>
<div ref="chartRef" class="chart-container"></div>
<div class="map-stage" :class="{ 'map-stage--local': currentMode === 'local' }">
<div class="map-tilt-layer">
<img class="map-world-bg" :src="mapWorldBgSrc" alt="" />
<div ref="chartRef" class="chart-container"></div>
</div>
<div class="map-legend" aria-hidden="true">
<img class="map-legend-bg" :src="LEGEND_BG" alt="" />
<ul class="map-legend-list">
@ -107,7 +86,7 @@
</template>
<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 BaseCard from './BaseCard.vue'
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 FALLBACK_TRADING_JSON = './yak-trading-data.json'
const emit = defineEmits(['mode-change'])
const chartRef = ref(null)
const sourceChartRef = ref(null)
const salesChartRef = ref(null)
@ -129,8 +110,8 @@ let mapSystemConfig = null
let cachedOutlineKey = ''
let cachedOutlinePaths = []
let unitedMapKey = ''
let mapBgImage = null
let mapBgImagePromise = null
const mapBgImageCache = new Map()
const mapBgImageLoading = new Map()
let mapSurfaceTexture = null
let mapSurfaceTextureKey = ''
@ -177,15 +158,25 @@ const MAP_WIREFRAME_OFFSET = -0.72
const MAP_WIREFRAME_OFFSET_LNG = 0.1
const MAP_WIREFRAME_OFFSET_LOCAL = -3.5
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_WIDTH = 0.45
const MAP_SHADOW_FILL_FAR = '#010204'
const MAP_SHADOW_FILL_MID = '#020408'
const MAP_SHADOW_FILL_NEAR = '#030a12'
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_HEIGHT = 1078
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_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 沿线
const METEOR_EFFECT_PATH = 'path://M0.5,0 L0.56,0.14 L0.52,1 L0.48,1 L0.44,0.14 Z'
@ -279,7 +276,7 @@ const loadData = async (mapMode = 'china') => {
if (!tradingData) {
return false
}
//
const mapName = getMapName(mapMode)
echarts.registerMap(mapName, chinaMapData)
@ -289,7 +286,7 @@ const loadData = async (mapMode = 'china') => {
mapSurfaceTextureKey = ''
cachedOutlineKey = ''
cachedOutlinePaths = []
return true
} catch (error) {
console.error('数据加载失败:', error)
@ -328,13 +325,13 @@ const generateScatterData = () => {
const modeData = getCurrentModeData()
const scatterData = []
const citySet = new Set()
//
modeData.flows.forEach(flow => {
citySet.add(flow.from)
citySet.add(flow.to)
})
//
citySet.forEach(city => {
const coord = tradingData.geoCoordMap[city]
@ -347,7 +344,7 @@ const generateScatterData = () => {
totalFlow += flow.value
}
})
scatterData.push({
name: city,
value: coord.concat([totalFlow]),
@ -363,7 +360,7 @@ const generateScatterData = () => {
})
}
})
return scatterData
}
@ -393,13 +390,13 @@ const generateEffectScatterData = () => {
const modeData = getCurrentModeData()
const effectData = []
const cityFlows = {}
//
modeData.flows.forEach(flow => {
cityFlows[flow.from] = (cityFlows[flow.from] || 0) + flow.value
cityFlows[flow.to] = (cityFlows[flow.to] || 0) + flow.value
})
//
const flowThreshold = tradingData?.flowLevels?.medium?.threshold || 40
const hubName = getHubName()
@ -421,7 +418,7 @@ const generateEffectScatterData = () => {
}
}
})
return effectData
}
@ -429,7 +426,7 @@ const generateEffectScatterData = () => {
const getCitySourceData = (cityName) => {
const modeData = getCurrentModeData()
const sources = []
modeData.flows.forEach(flow => {
if (flow.to === cityName) {
sources.push({
@ -439,7 +436,7 @@ const getCitySourceData = (cityName) => {
})
}
})
return sources
}
@ -447,7 +444,7 @@ const getCitySourceData = (cityName) => {
const getCitySalesData = (cityName) => {
const modeData = getCurrentModeData()
const sales = []
modeData.flows.forEach(flow => {
if (flow.from === cityName) {
sales.push({
@ -457,7 +454,7 @@ const getCitySalesData = (cityName) => {
})
}
})
return sales
}
@ -487,7 +484,7 @@ const createPieOption = (data, title) => {
itemWidth: 10,
itemHeight: 8,
itemGap: 8,
formatter: function(name) {
formatter: function (name) {
const item = data.find(d => d.name === name)
return item ? `${name}(${item.value})` : name
}
@ -526,7 +523,7 @@ const createPieOption = (data, title) => {
const showFloatingPanels = (cityName) => {
const sources = getCitySourceData(cityName)
const sales = getCitySalesData(cityName)
if (sources.length > 0) {
sourceData.value = sources
showSourcePanel.value = true
@ -539,7 +536,7 @@ const showFloatingPanels = (cityName) => {
}
})
}
if (sales.length > 0) {
salesData.value = sales
showSalesPanel.value = true
@ -560,7 +557,7 @@ const hideFloatingPanels = () => {
showSalesPanel.value = false
sourceData.value = null
salesData.value = null
if (sourceChartInstance) {
sourceChartInstance.dispose()
sourceChartInstance = null
@ -636,7 +633,7 @@ const generateDetailData = (cityName) => {
contactPhone: '134****6789'
}
]
//
if (cityName === getHubName()) {
return baseData
@ -660,7 +657,7 @@ const showDetailData = (cityName) => {
selectedCity.value = cityName
detailData.value = generateDetailData(cityName)
showDetailPanel.value = true
//
hideFloatingPanels()
}
@ -742,19 +739,26 @@ const registerUnitedMap = (mapMode = currentMode.value) => {
return unitedName
}
const getMapBgCrop = () => (
currentMode.value === 'local' ? MAP_BG_CROP_LOCAL : MAP_BG_CROP_CHINA
)
const getMapBgCrop = (image) => {
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 = () => {
if (mapBgImage) {
return Promise.resolve(mapBgImage)
const loadMapBgImage = (mode = currentMode.value) => {
const src = getMapBgImageSrc(mode)
if (mapBgImageCache.has(src)) {
return Promise.resolve(mapBgImageCache.get(src))
}
if (mapBgImagePromise) {
return mapBgImagePromise
if (mapBgImageLoading.has(src)) {
return mapBgImageLoading.get(src)
}
mapBgImagePromise = new Promise((resolve, reject) => {
const promise = new Promise((resolve, reject) => {
if (typeof Image === 'undefined') {
reject(new Error('Image is not available'))
return
@ -763,17 +767,19 @@ const loadMapBgImage = () => {
const image = new Image()
image.decoding = 'async'
image.onload = () => {
mapBgImage = image
mapBgImageCache.set(src, image)
mapBgImageLoading.delete(src)
resolve(image)
}
image.onerror = () => {
mapBgImagePromise = null
reject(new Error(`Failed to load ${MAP_BG_IMAGE}`))
mapBgImageLoading.delete(src)
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) => {
@ -846,7 +852,7 @@ const createMapSurfaceTexture = (image) => {
return null
}
paintMapSurfaceTexture(ctx, width, height, image, getMapBgCrop())
paintMapSurfaceTexture(ctx, width, height, image, getMapBgCrop(image))
return canvas
}
@ -856,7 +862,7 @@ const ensureMapSurfaceTexture = async () => {
return mapSurfaceTexture
}
const image = await loadMapBgImage()
const image = await loadMapBgImage(key)
mapSurfaceTexture = createMapSurfaceTexture(image)
mapSurfaceTextureKey = key
return mapSurfaceTexture
@ -964,8 +970,8 @@ const stitchBoundarySegments = (segments) => {
return true
}
while (extendTail()) {}
while (extendHead()) {}
while (extendTail()) { }
while (extendHead()) { }
paths.push(path)
})
@ -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 modeData = getCurrentModeData()
@ -1151,7 +1175,7 @@ const getChartOption = () => {
const mapLayout = getMapProjection(mapBounds, 0)
const outlinePaths = getWireframePaths()
const wireframeData = outlinePaths.map((coords) => ({ coords }))
return {
backgroundColor: 'transparent', //
// title: {
@ -1177,7 +1201,7 @@ const getChartOption = () => {
color: '#fff',
fontSize: 13
},
formatter: function(params) {
formatter: function (params) {
if (params.componentType === 'geo') {
return `<div style="padding: 8px;">
<strong style="color: #00d4ff;">${params.name}</strong><br/>
@ -1186,16 +1210,16 @@ const getChartOption = () => {
} else if (params.seriesType === 'scatter') {
const value = params.value[2] || 0
const hubName = getHubName()
const nodeType = params.name === hubName ? '中心节点' :
(currentMode.value === 'outflow' ? '销售市场' : '供应来源')
const nodeType = params.name === hubName ? '中心节点' :
(currentMode.value === 'outflow' ? '销售市场' : '供应来源')
return `<div style="padding: 8px;">
<strong style="color: #00d4ff;">${params.name}</strong><br/>
<span style="color: #67c23a;">总流量: ${value} </span><br/>
<span style="color: #8cc8ff;">节点类型: ${nodeType}</span>
</div>`
} else if (params.seriesType === 'lines') {
const lineData = linesData.find(line =>
line.fromName === params.data.fromName &&
const lineData = linesData.find(line =>
line.fromName === params.data.fromName &&
line.toName === params.data.toName
)
return `<div style="padding: 8px;">
@ -1243,25 +1267,23 @@ const getChartOption = () => {
disabled: true
}
},
{
name: 'mapWireframe',
type: 'lines',
coordinateSystem: 'geo',
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)'
...MAP_WIREFRAME_HALO_LAYERS.map((layer, index) => buildWireframeLineSeries(
`mapWireframeHalo${index}`,
4,
wireframeData,
{
color: layer.color,
width: layer.width,
opacity: 1
}
},
)),
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: '交易城市',
type: 'scatter',
@ -1347,7 +1369,7 @@ const getChartOption = () => {
geoIndex: GEO_DATA_INDEX,
zlevel: 6,
data: effectScatterData,
symbolSize: function(val) {
symbolSize: function (val) {
return Math.max(14, Math.min(26, val[2] / 60))
},
showEffectOn: 'render',
@ -1390,7 +1412,7 @@ const reloadMapData = async (mapMode) => {
const mapFilePath = getMapFilePath(mapMode)
const mapResponse = await fetch(mapFilePath)
chinaMapData = await mapResponse.json()
const mapName = getMapName(mapMode)
echarts.registerMap(mapName, chinaMapData)
unitedMapKey = ''
@ -1399,7 +1421,7 @@ const reloadMapData = async (mapMode) => {
mapSurfaceTextureKey = ''
cachedOutlineKey = ''
cachedOutlinePaths = []
return true
} catch (error) {
console.error('地图数据重新加载失败:', error)
@ -1414,34 +1436,34 @@ const handleChartResize = () => {
//
const initChart = async () => {
if (!chartRef.value) return
const dataLoaded = await loadData(currentMode.value)
if (!dataLoaded) return
chartInstance = echarts.init(chartRef.value)
chartInstance.setOption(getChartOption())
await refreshMapSurfaceTexture()
//
chartInstance.on('mouseover', (params) => {
if (params.seriesType === 'scatter' || params.seriesType === 'effectScatter') {
showFloatingPanels(params.name)
}
})
chartInstance.on('mouseout', (params) => {
if (params.seriesType === 'scatter' || params.seriesType === 'effectScatter') {
hideFloatingPanels()
}
})
//
chartInstance.on('click', (params) => {
if (params.seriesType === 'scatter' || params.seriesType === 'effectScatter') {
showDetailData(params.name)
}
})
window.addEventListener('resize', handleChartResize)
const mountPoint = chartRef.value.parentElement
if (mountPoint) {
@ -1463,7 +1485,7 @@ const updateChart = async (needReloadMap = false) => {
const mapLoaded = await reloadMapData(currentMode.value)
if (!mapLoaded) return
}
chartInstance.setOption(getChartOption(), {
notMerge: false,
replaceMerge: ['geo', 'series']
@ -1476,15 +1498,17 @@ const updateChart = async (needReloadMap = false) => {
//
watch(currentMode, (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 })
onMounted(() => {
initChart()
@ -1602,6 +1626,38 @@ onUnmounted(() => {
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 {
position: absolute;
width: var(--screen-width, 5120px);
@ -1856,4 +1912,4 @@ onUnmounted(() => {
.detail-content::-webkit-scrollbar-thumb:hover {
background: rgba(0, 212, 255, 0.7);
}
</style>
</style>

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

Loading…
Cancel
Save