Refactor ChinaMap and ExchangeMonitor components for improved functionality and layout; update background image handling in ChinaMap; enhance service status representation in ExchangeMonitor; adjust styles for better visual consistency across components.

main
Swanky 6 days ago
parent 51723351d1
commit 2954dc2e92
  1. 518
      src/components/ChinaMap.vue
  2. 121
      src/components/ExchangeMonitor.vue
  3. 2
      src/components/HeaderBar.vue
  4. 2
      src/views/Dashboard.vue

@ -79,6 +79,11 @@
</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-legend" aria-hidden="true">
<img class="map-legend-bg" :src="LEGEND_BG" alt="" />
@ -117,11 +122,17 @@ const currentMode = ref('outflow')
let chartInstance = null
let sourceChartInstance = null
let salesChartInstance = null
let resizeObserver = null
let chinaMapData = null
let tradingData = null
let mapSystemConfig = null
let cachedOutlineKey = ''
let cachedOutlinePaths = []
let unitedMapKey = ''
let mapBgImage = null
let mapBgImagePromise = null
let mapSurfaceTexture = null
let mapSurfaceTextureKey = ''
//
const showSourcePanel = ref(false)
@ -143,47 +154,50 @@ const modes = [
const BTN_NORMAL = '/images/按钮.png'
const BTN_ACTIVE = '/images/按钮选中.png'
const LEGEND_BG = '/images/图例bg.png'
const GEO_DATA_INDEX = 2
const WIREFRAME_GEO_INDEX = 3
const GEO_DATA_INDEX = 4
const WIREFRAME_GEO_INDEX = 5
const MAP_ASPECT_SCALE = 0.82
const MAP_ZOOM_FACTOR = 0.97
const MAP_LAYOUT_SIZE = '84%'
const MAP_ANCHOR_CENTER = [104.8, 37.2]
const MAP_ANCHOR_ZOOM = 1.05
const MAP_GLOBAL_OFFSET_LNG = 1.45
const MAP_GLOBAL_OFFSET_LAT = 0.9
const MAP_GLOBAL_LAYOUT_X = -2.8
const MAP_GLOBAL_LAYOUT_Y = 3.6
const MAP_SHADOW_OFFSET_FAR = 1.25
const MAP_SHADOW_OFFSET_NEAR = 0.75
const MAP_SHADOW_OFFSET_LNG = -0.45
const MAP_ZOOM_FACTOR = 1
const MAP_LAYOUT_SIZE = '88%'
const MAP_ANCHOR_CENTER = [105.2, 36.8]
const MAP_ANCHOR_ZOOM = 1.08
const MAP_GLOBAL_OFFSET_LNG = 1.2
const MAP_GLOBAL_OFFSET_LAT = 0.85
const MAP_GLOBAL_LAYOUT_X = -2.2
const MAP_GLOBAL_LAYOUT_Y = 2.8
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_NEAR_LOCAL = 3
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_WIREFRAME_OFFSET = -0.65
const MAP_WIREFRAME_OFFSET_LNG = 0.12
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_FILL_WEST = [45, 181, 181]
const MAP_FILL_EAST = [12, 123, 176]
const MAP_GRADIENT_MIN_LNG = 73
const MAP_GRADIENT_MAX_LNG = 135
const MAP_GRADIENT_MIN_LAT = 18
const MAP_GRADIENT_MAX_LAT = 54
const MAP_WIREFRAME_COLOR = '#f8feff'
const MAP_INNER_BORDER = 'rgba(6, 38, 62, 0.62)'
const MAP_INNER_BORDER_WIDTH = 0.8
const MAP_SHADOW_FAR = '#000000'
const MAP_SHADOW_NEAR = '#000000'
const MAP_WIREFRAME_COLOR = '#f2fdff'
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'
const MAP_FILL_OPACITY = 0.88
const MAP_TEXTURE_SIZE = 128
// 4318×1078
const MAP_BG_IMAGE = '/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 }
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_COLOR = 'rgba(0, 0, 0, 0)'
// 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 mapTextureCache = new Map()
//
const getMapFilePath = (mode) => {
switch (mode) {
@ -269,9 +283,12 @@ const loadData = async (mapMode = 'china') => {
//
const mapName = getMapName(mapMode)
echarts.registerMap(mapName, chinaMapData)
unitedMapKey = ''
registerUnitedMap(mapMode)
mapSurfaceTexture = null
mapSurfaceTextureKey = ''
cachedOutlineKey = ''
cachedOutlinePaths = []
mapTextureCache.clear()
return true
} catch (error) {
@ -669,8 +686,6 @@ const calculateMapBounds = () => {
}
}
const clamp = (value, min, max) => Math.min(max, Math.max(min, value))
const getFeatureCenter = (feature) => {
const cp = feature?.properties?.cp || feature?.properties?.center
if (Array.isArray(cp) && cp.length >= 2) {
@ -682,91 +697,201 @@ const getFeatureCenter = (feature) => {
return [104, 35]
}
const getRegionAreaColor = (lng, lat, minLng, maxLng, minLat, maxLat) => {
const { r, g, b } = blendDesignColor(lng, lat, minLng, maxLng, minLat, maxLat)
const cacheKey = `${r},${g},${b},${MAP_FILL_OPACITY}`
const getUnitedMapName = (mode = currentMode.value) => `${getMapName(mode)}-united`
if (!mapTextureCache.has(cacheKey)) {
mapTextureCache.set(cacheKey, createRegionTextureCanvas(r, g, b, MAP_FILL_OPACITY))
const buildUnitedGeoJson = (geojson) => {
if (!geojson?.features?.length) {
return geojson
}
const coordinates = []
geojson.features.forEach((feature) => {
const geom = feature.geometry
if (!geom) {
return
}
if (geom.type === 'Polygon') {
coordinates.push(geom.coordinates)
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach((polygon) => coordinates.push(polygon))
}
})
return {
image: mapTextureCache.get(cacheKey),
repeat: 'repeat'
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: { name: 'united' },
geometry: {
type: 'MultiPolygon',
coordinates
}
}]
}
}
const createRegionTextureCanvas = (r, g, b, alpha) => {
if (typeof document === 'undefined') {
return null
const registerUnitedMap = (mapMode = currentMode.value) => {
const unitedName = getUnitedMapName(mapMode)
const key = `${unitedName}-${chinaMapData?.features?.length || 0}`
if (unitedMapKey !== key) {
echarts.registerMap(unitedName, buildUnitedGeoJson(chinaMapData))
unitedMapKey = key
mapSurfaceTexture = null
mapSurfaceTextureKey = ''
}
return unitedName
}
const getMapBgCrop = () => (
currentMode.value === 'local' ? MAP_BG_CROP_LOCAL : MAP_BG_CROP_CHINA
)
const size = MAP_TEXTURE_SIZE
const loadMapBgImage = () => {
if (mapBgImage) {
return Promise.resolve(mapBgImage)
}
if (mapBgImagePromise) {
return mapBgImagePromise
}
mapBgImagePromise = new Promise((resolve, reject) => {
if (typeof Image === 'undefined') {
reject(new Error('Image is not available'))
return
}
const image = new Image()
image.decoding = 'async'
image.onload = () => {
mapBgImage = image
resolve(image)
}
image.onerror = () => {
mapBgImagePromise = null
reject(new Error(`Failed to load ${MAP_BG_IMAGE}`))
}
image.src = MAP_BG_IMAGE
})
return mapBgImagePromise
}
const paintMapSurfaceTexture = (ctx, width, height, image, crop) => {
ctx.clearRect(0, 0, width, height)
// 1.
ctx.filter = 'brightness(1.28) contrast(1.18)'
ctx.drawImage(
image,
crop.x,
crop.y,
crop.width,
crop.height,
0,
0,
width,
height
)
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)
// 3. soft-light
ctx.globalCompositeOperation = 'soft-light'
ctx.fillStyle = 'rgba(118, 218, 214, 0.36)'
ctx.fillRect(0, 0, width, height)
// 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)
ctx.globalCompositeOperation = 'source-over'
}
const createMapSurfaceTexture = (image) => {
const width = MAP_SURFACE_TEXTURE_WIDTH
const height = MAP_SURFACE_TEXTURE_HEIGHT
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) {
return null
}
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`
ctx.fillRect(0, 0, size, size)
for (let i = 0; i < 420; i += 1) {
const x = Math.random() * size
const y = Math.random() * size
const radius = Math.random() * 1.6 + 0.3
ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.1 + 0.02})`
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.fill()
}
for (let i = 0; i < 260; i += 1) {
const x = Math.random() * size
const y = Math.random() * size
const radius = Math.random() * 2.4 + 0.6
ctx.fillStyle = `rgba(8, 45, 72, ${Math.random() * 0.08 + 0.02})`
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.fill()
}
paintMapSurfaceTexture(ctx, width, height, image, getMapBgCrop())
return canvas
}
for (let i = 0; i < 6; i += 1) {
const x = Math.random() * size
const y = Math.random() * size
const radius = Math.random() * 28 + 18
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius)
gradient.addColorStop(0, `rgba(255, 255, 255, ${Math.random() * 0.06 + 0.02})`)
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
ctx.fillStyle = gradient
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.fill()
const ensureMapSurfaceTexture = async () => {
const key = currentMode.value
if (mapSurfaceTexture && mapSurfaceTextureKey === key) {
return mapSurfaceTexture
}
return canvas
const image = await loadMapBgImage()
mapSurfaceTexture = createMapSurfaceTexture(image)
mapSurfaceTextureKey = key
return mapSurfaceTexture
}
const blendDesignColor = (lng, lat, minLng, maxLng, minLat, maxLat) => {
const lngRatio = clamp((lng - minLng) / (maxLng - minLng), 0, 1)
const latRatio = clamp((lat - minLat) / (maxLat - minLat), 0, 1)
const getMapSurfaceAreaStyle = () => {
if (mapSurfaceTexture) {
return {
areaColor: {
image: mapSurfaceTexture,
repeat: 'no-repeat'
}
}
}
let r = MAP_FILL_WEST[0] + (MAP_FILL_EAST[0] - MAP_FILL_WEST[0]) * lngRatio
let g = MAP_FILL_WEST[1] + (MAP_FILL_EAST[1] - MAP_FILL_WEST[1]) * lngRatio
let b = MAP_FILL_WEST[2] + (MAP_FILL_EAST[2] - MAP_FILL_WEST[2]) * lngRatio
return { areaColor: MAP_SURFACE_FALLBACK }
}
const northBoost = (0.62 - latRatio) * 16
const southDepth = (latRatio - 0.38) * 10
r = clamp(r + northBoost - southDepth * 0.35, 0, 255)
g = clamp(g + northBoost * 0.95 - southDepth * 0.25, 0, 255)
b = clamp(b + northBoost * 0.55 - southDepth * 0.15, 0, 255)
const refreshMapSurfaceTexture = async () => {
if (!chartInstance) {
return
}
return {
r: Math.round(r),
g: Math.round(g),
b: Math.round(b)
try {
await ensureMapSurfaceTexture()
const mapBounds = calculateMapBounds()
const mapName = getMapName(currentMode.value)
const unitedMapName = registerUnitedMap(currentMode.value)
chartInstance.setOption({
geo: buildGeoLayers(mapName, unitedMapName, mapBounds)
}, {
replaceMerge: ['geo']
})
} catch (error) {
console.warn('地图纹理生成失败:', error)
}
}
@ -898,49 +1023,6 @@ const getWireframePaths = () => {
return cachedOutlinePaths
}
const buildRegionFillData = () => {
if (!chinaMapData?.features?.length) {
return []
}
let minLng = MAP_GRADIENT_MIN_LNG
let maxLng = MAP_GRADIENT_MAX_LNG
let minLat = MAP_GRADIENT_MIN_LAT
let maxLat = MAP_GRADIENT_MAX_LAT
if (currentMode.value === 'local') {
const centers = chinaMapData.features.map(getFeatureCenter)
const lngs = centers.map((center) => center[0])
const lats = centers.map((center) => center[1])
minLng = Math.min(...lngs)
maxLng = Math.max(...lngs)
minLat = Math.min(...lats)
maxLat = Math.max(...lats)
if (maxLng - minLng < 0.001) {
maxLng = minLng + 1
}
if (maxLat - minLat < 0.001) {
maxLat = minLat + 1
}
}
return chinaMapData.features.map((feature) => {
const name = feature.properties?.name
if (!name) {
return null
}
const [lng, lat] = getFeatureCenter(feature)
return {
name,
itemStyle: {
areaColor: getRegionAreaColor(lng, lat, minLng, maxLng, minLat, maxLat),
borderColor: MAP_INNER_BORDER,
borderWidth: MAP_INNER_BORDER_WIDTH
}
}
}).filter(Boolean)
}
const getMapProjection = (mapBounds, latOffset = 0, lngOffset = 0) => {
const isLocal = currentMode.value === 'local'
if (isLocal) {
@ -965,41 +1047,59 @@ const getMapProjection = (mapBounds, latOffset = 0, lngOffset = 0) => {
}
}
const buildGeoLayers = (mapName, mapBounds) => {
const buildShadowFillGeo = (unitedMapName, mapBounds, latOffset, lngOffset, areaColor, z) => ({
map: unitedMapName,
roam: false,
...getMapProjection(mapBounds, latOffset, lngOffset),
silent: true,
zlevel: 0,
z,
label: { show: false },
itemStyle: {
areaColor,
borderColor: 'transparent',
borderWidth: 0
},
emphasis: {
disabled: true
}
})
const getShadowOffsets = () => {
const isLocal = currentMode.value === 'local'
return {
far: isLocal ? MAP_SHADOW_OFFSET_FAR_LOCAL : MAP_SHADOW_OFFSET_FAR,
mid: isLocal ? MAP_SHADOW_OFFSET_MID_LOCAL : MAP_SHADOW_OFFSET_MID,
near: isLocal ? MAP_SHADOW_OFFSET_NEAR_LOCAL : MAP_SHADOW_OFFSET_NEAR,
lng: isLocal ? MAP_SHADOW_OFFSET_LNG_LOCAL : MAP_SHADOW_OFFSET_LNG
}
}
const buildGeoLayers = (mapName, unitedMapName, mapBounds) => {
const isLocal = currentMode.value === 'local'
const shadowFarOffset = isLocal ? MAP_SHADOW_OFFSET_FAR_LOCAL : MAP_SHADOW_OFFSET_FAR
const shadowNearOffset = isLocal ? MAP_SHADOW_OFFSET_NEAR_LOCAL : MAP_SHADOW_OFFSET_NEAR
const shadowLngOffset = isLocal ? MAP_SHADOW_OFFSET_LNG_LOCAL : MAP_SHADOW_OFFSET_LNG
const wireframeOffset = isLocal ? MAP_WIREFRAME_OFFSET_LOCAL : MAP_WIREFRAME_OFFSET
const wireframeLngOffset = isLocal ? MAP_WIREFRAME_OFFSET_LNG_LOCAL : MAP_WIREFRAME_OFFSET_LNG
const { far, mid, near, lng } = getShadowOffsets()
return [
buildShadowFillGeo(unitedMapName, mapBounds, far, lng, MAP_SHADOW_FILL_FAR, 1),
buildShadowFillGeo(unitedMapName, mapBounds, mid, lng, MAP_SHADOW_FILL_MID, 2),
buildShadowFillGeo(unitedMapName, mapBounds, near, lng, MAP_SHADOW_FILL_NEAR, 3),
{
map: mapName,
roam: false,
...getMapProjection(mapBounds, shadowFarOffset, shadowLngOffset),
silent: true,
zlevel: 0,
z: 1,
label: { show: false },
itemStyle: {
areaColor: MAP_SHADOW_FAR,
borderColor: MAP_SHADOW_FAR,
borderWidth: 0
}
},
{
map: mapName,
map: unitedMapName,
roam: false,
...getMapProjection(mapBounds, shadowNearOffset, shadowLngOffset),
...getMapProjection(mapBounds, 0),
silent: true,
zlevel: 0,
zlevel: 1,
z: 2,
label: { show: false },
itemStyle: {
areaColor: MAP_SHADOW_NEAR,
borderColor: MAP_SHADOW_NEAR,
...getMapSurfaceAreaStyle(),
borderColor: 'transparent',
borderWidth: 0
},
emphasis: {
disabled: true
}
},
{
@ -1047,6 +1147,7 @@ const getChartOption = () => {
const effectScatterData = generateEffectScatterData()
const mapBounds = calculateMapBounds()
const mapName = getMapName(currentMode.value)
const unitedMapName = registerUnitedMap(currentMode.value)
const mapLayout = getMapProjection(mapBounds, 0)
const outlinePaths = getWireframePaths()
const wireframeData = outlinePaths.map((coords) => ({ coords }))
@ -1113,7 +1214,7 @@ const getChartOption = () => {
return params.name
}
},
geo: buildGeoLayers(mapName, mapBounds),
geo: buildGeoLayers(mapName, unitedMapName, mapBounds),
series: [
{
name: 'mapFill',
@ -1124,18 +1225,18 @@ const getChartOption = () => {
zlevel: 2,
silent: true,
selectedMode: false,
data: buildRegionFillData(),
itemStyle: {
areaColor: MAP_SURFACE_COLOR,
borderColor: MAP_INNER_BORDER,
borderWidth: MAP_INNER_BORDER_WIDTH
},
label: {
show: true,
color: MAP_LABEL_COLOR,
fontSize: 11,
fontSize: 12,
fontFamily: 'Microsoft YaHei, sans-serif',
fontWeight: 'bold',
textBorderColor: 'rgba(0, 0, 0, 0.45)',
textBorderColor: 'rgba(0, 0, 0, 0.5)',
textBorderWidth: 2
},
emphasis: {
@ -1153,12 +1254,12 @@ const getChartOption = () => {
data: wireframeData,
lineStyle: {
color: MAP_WIREFRAME_COLOR,
width: 3.2,
opacity: 0.96,
width: 4.2,
opacity: 0.98,
cap: 'round',
join: 'round',
shadowBlur: 16,
shadowColor: 'rgba(120, 230, 255, 0.6)'
shadowBlur: 22,
shadowColor: 'rgba(130, 245, 255, 0.78)'
}
},
{
@ -1200,12 +1301,12 @@ const getChartOption = () => {
zlevel: 6,
data: linesData,
lineStyle: {
color: '#7ee8ff',
width: 1.2,
opacity: 0.75,
curveness: 0.28,
shadowBlur: 8,
shadowColor: 'rgba(110, 232, 255, 0.55)'
color: '#a8f6ff',
width: 1.5,
opacity: 0.82,
curveness: 0.32,
shadowBlur: 10,
shadowColor: 'rgba(120, 240, 255, 0.65)'
},
emphasis: {
lineStyle: {
@ -1226,17 +1327,17 @@ const getChartOption = () => {
data: linesData,
effect: {
show: true,
constantSpeed: 50,
constantSpeed: 55,
symbol: METEOR_EFFECT_PATH,
symbolSize: [5, 20],
trailLength: 0.14,
symbolSize: [5, 22],
trailLength: 0.16,
loop: true,
color: '#ffffff'
color: '#e8fbff'
},
lineStyle: {
width: 0,
opacity: 0,
curveness: 0.28
curveness: 0.32
}
},
{
@ -1292,9 +1393,12 @@ const reloadMapData = async (mapMode) => {
const mapName = getMapName(mapMode)
echarts.registerMap(mapName, chinaMapData)
unitedMapKey = ''
registerUnitedMap(mapMode)
mapSurfaceTexture = null
mapSurfaceTextureKey = ''
cachedOutlineKey = ''
cachedOutlinePaths = []
mapTextureCache.clear()
return true
} catch (error) {
@ -1303,6 +1407,10 @@ const reloadMapData = async (mapMode) => {
}
}
const handleChartResize = () => {
chartInstance?.resize()
}
//
const initChart = async () => {
if (!chartRef.value) return
@ -1312,6 +1420,7 @@ const initChart = async () => {
chartInstance = echarts.init(chartRef.value)
chartInstance.setOption(getChartOption())
await refreshMapSurfaceTexture()
//
chartInstance.on('mouseover', (params) => {
@ -1333,9 +1442,17 @@ const initChart = async () => {
}
})
//
window.addEventListener('resize', () => {
chartInstance?.resize()
window.addEventListener('resize', handleChartResize)
const mountPoint = chartRef.value.parentElement
if (mountPoint) {
resizeObserver = new ResizeObserver(handleChartResize)
resizeObserver.observe(mountPoint)
}
await nextTick()
requestAnimationFrame(() => {
handleChartResize()
setTimeout(handleChartResize, 160)
})
}
@ -1351,6 +1468,9 @@ const updateChart = async (needReloadMap = false) => {
notMerge: false,
replaceMerge: ['geo', 'series']
})
await refreshMapSurfaceTexture()
await nextTick()
handleChartResize()
}
}
@ -1371,6 +1491,9 @@ onMounted(() => {
})
onUnmounted(() => {
window.removeEventListener('resize', handleChartResize)
resizeObserver?.disconnect()
resizeObserver = null
if (chartInstance) {
chartInstance.dispose()
}
@ -1388,12 +1511,15 @@ onUnmounted(() => {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
background: transparent;
}
.map-card :deep(.card-content) {
position: relative;
height: 100%;
min-height: 0;
padding: 0;
}
@ -1473,15 +1599,31 @@ onUnmounted(() => {
position: absolute;
inset: 0;
overflow: hidden;
background: transparent;
}
.map-world-bg {
position: absolute;
width: var(--screen-width, 5120px);
height: var(--screen-height, 1440px);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
object-fit: fill;
pointer-events: none;
user-select: none;
z-index: 0;
}
.chart-container {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
width: 100% !important;
height: 100% !important;
min-width: 0;
min-height: 0;
z-index: 1;
transform: translate(-1.2%, 1.6%);
transform: translate(-0.4%, 0.6%);
}
.map-legend {

@ -24,7 +24,7 @@
</div>
</div>
</div>
<div class="service-value-wrap" :class="item.valueClass">
<div class="service-value-wrap" :class="item.statusClass">
<span class="service-value">{{ formatNumber(item.value) }}</span>
<span class="service-unit">{{ item.unit }}</span>
</div>
@ -39,14 +39,13 @@ import BaseCard from './BaseCard.vue'
const API_URL = '/api/dashboard/exchange-service-info'
const serviceData = ref([
const SERVICE_ITEMS = [
{
key: 'totalSupply',
label: '牦牛供应总量',
value: 0,
unit: '头',
icon: '/images/牦牛.png',
valueClass: 'value-green',
status: '--',
statusClass: 'normal'
},
@ -56,7 +55,6 @@ const serviceData = ref([
value: 0,
unit: '头',
icon: '/images/牦牛.png',
valueClass: 'value-cyan',
status: '--',
statusClass: 'normal'
},
@ -66,7 +64,6 @@ const serviceData = ref([
value: 0,
unit: '头',
icon: '/images/牦牛.png',
valueClass: 'value-green',
status: '--',
statusClass: 'normal'
},
@ -76,7 +73,6 @@ const serviceData = ref([
value: 0,
unit: '辆',
icon: '/images/进场车辆.png',
valueClass: 'value-cyan',
status: '--',
statusClass: 'normal'
},
@ -86,7 +82,6 @@ const serviceData = ref([
value: 0,
unit: '个',
icon: '/images/剩余车位.png',
valueClass: 'value-red',
status: '--',
statusClass: 'normal'
},
@ -96,11 +91,12 @@ const serviceData = ref([
value: 0,
unit: '家',
icon: '/images/供应商数量.png',
valueClass: 'value-blue',
status: '--',
statusClass: 'normal'
}
])
]
const serviceData = ref(SERVICE_ITEMS.map((item) => ({ ...item })))
const formatNumber = (num) => {
const value = Number(num) || 0
@ -144,21 +140,23 @@ const resolveSupplierStatus = (value) => {
if (value > 50) {
return { status: '正常', statusClass: 'normal' }
}
return { status: '较少', statusClass: 'low' }
return { status: '较少', statusClass: 'tight' }
}
const tagToStatusClass = (tag) => {
const text = String(tag || '').trim()
const map = {
充足: 'abundant',
正常: 'normal',
紧张: 'tight',
活跃: 'active',
较少: 'low'
较少: 'tight'
}
return map[tag] || 'normal'
return map[text] || 'normal'
}
const applyStatus = (item, data) => {
const buildServiceItem = (template, data) => {
const item = { ...template }
const value = Number(data[item.key]) || 0
item.value = value
@ -169,31 +167,31 @@ const applyStatus = (item, data) => {
}
if (tag) {
item.status = tag
item.statusClass = tagToStatusClass(tag)
return
item.status = String(tag).trim()
item.statusClass = tagToStatusClass(item.status)
return item
}
if (item.key === 'totalSupply') {
Object.assign(item, resolveSupplyStatus(value))
return
return item
}
if (item.key === 'forSaleYaks') {
Object.assign(item, resolveForSaleStatus(value))
return
return item
}
if (item.key === 'remainingParking') {
Object.assign(item, resolveParkingStatus(value))
return
return item
}
if (item.key === 'supplierCount') {
Object.assign(item, resolveSupplierStatus(value))
return
}
if (item.key === 'soldYaks' || item.key === 'enteringVehicles') {
item.status = '正常'
item.statusClass = 'normal'
return item
}
item.status = '正常'
item.statusClass = 'normal'
return item
}
const loadData = async () => {
@ -204,13 +202,12 @@ const loadData = async () => {
}
const result = await response.json()
if (result.code !== 1 || !result.data) {
if (result.code !== undefined && result.code !== 1) {
throw new Error(result.message || '接口返回异常')
}
const payload = result.data ?? result
serviceData.value.forEach((item) => {
applyStatus(item, result.data)
})
serviceData.value = SERVICE_ITEMS.map((template) => buildServiceItem(template, payload))
} catch (error) {
console.error('加载交易中心实时服务信息失败:', error)
}
@ -328,34 +325,60 @@ onUnmounted(() => {
font-weight: 500;
}
.service-status.abundant {
.service-status.abundant,
.service-value-wrap.abundant .service-value {
color: #1afc7a;
}
.service-status.abundant {
border-color: rgba(26, 252, 122, 0.55);
background: rgba(26, 252, 122, 0.08);
}
.service-status.normal {
.service-value-wrap.abundant .service-value {
text-shadow: 0 0 8px rgba(26, 252, 122, 0.45);
}
.service-status.normal,
.service-value-wrap.normal .service-value {
color: #0385f2;
}
.service-status.normal {
border-color: rgba(3, 133, 242, 0.55);
background: rgba(3, 133, 242, 0.08);
}
.service-status.tight {
.service-value-wrap.normal .service-value {
text-shadow: 0 0 8px rgba(3, 133, 242, 0.45);
}
.service-status.tight,
.service-value-wrap.tight .service-value {
color: #fe4a46;
}
.service-status.tight {
border-color: rgba(254, 74, 70, 0.55);
background: rgba(254, 74, 70, 0.08);
}
.service-value-wrap.tight .service-value {
text-shadow: 0 0 8px rgba(254, 74, 70, 0.45);
}
.service-status.active,
.service-value-wrap.active .service-value {
color: #09fef7;
}
.service-status.active {
color: #0bfdf2;
border-color: rgba(11, 253, 242, 0.55);
background: rgba(11, 253, 242, 0.08);
border-color: rgba(9, 254, 247, 0.55);
background: rgba(9, 254, 247, 0.08);
}
.service-status.low {
color: #fe4a46;
border-color: rgba(254, 74, 70, 0.55);
background: rgba(254, 74, 70, 0.08);
.service-value-wrap.active .service-value {
text-shadow: 0 0 8px rgba(9, 254, 247, 0.45);
}
.service-value-wrap {
@ -370,7 +393,7 @@ onUnmounted(() => {
.service-value {
display: inline-block;
font-family: 'Arial Black', 'Microsoft YaHei', sans-serif;
font-size: 28px;
font-size: 24px;
font-weight: 700;
font-style: italic;
line-height: 1;
@ -388,24 +411,4 @@ onUnmounted(() => {
line-height: 1;
color: #d9f1ff;
}
.value-green .service-value {
color: #1afc7a;
text-shadow: 0 0 8px rgba(26, 252, 122, 0.45);
}
.value-blue .service-value {
color: #0385f2;
text-shadow: 0 0 8px rgba(3, 133, 242, 0.45);
}
.value-cyan .service-value {
color: #0bfdf2;
text-shadow: 0 0 8px rgba(11, 253, 242, 0.45);
}
.value-red .service-value {
color: #fe4a46;
text-shadow: 0 0 8px rgba(254, 74, 70, 0.45);
}
</style>

@ -56,7 +56,7 @@ const headerStyle = computed(() => ({
padding: 0 40px;
background-position: center center;
background-repeat: no-repeat;
background-size: 100% 100%;
background-size: 2340px 95px;
border-radius: 12px;
position: relative;
z-index: 10;

@ -371,12 +371,14 @@ onMounted(async () => {
display: flex;
flex-direction: column;
min-width: 0;
background: transparent;
}
.map-container {
position: relative;
flex: 1;
min-height: 0;
background: transparent;
}
.floating-table {

Loading…
Cancel
Save