Add echarts-gl dependency, update package-lock.json and package.json; enhance Vite config with API proxy; refactor App.vue and BaseCard.vue for improved layout; implement real-time data fetching in ExchangeMonitor and RealTimeStats components; optimize YakPriceTrend and YakTradingData components with new chart features.
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 409 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
@ -0,0 +1,89 @@ |
||||
<template> |
||||
<div class="date-indicator-tabs"> |
||||
<button |
||||
v-for="item in items" |
||||
:key="item.key" |
||||
type="button" |
||||
class="date-indicator-tab" |
||||
:class="{ active: modelValue === item.key }" |
||||
@click="$emit('update:modelValue', item.key)" |
||||
> |
||||
<span class="tab-text">{{ item.label }}</span> |
||||
</button> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
defineProps({ |
||||
modelValue: { |
||||
type: String, |
||||
required: true |
||||
}, |
||||
items: { |
||||
type: Array, |
||||
required: true |
||||
} |
||||
}) |
||||
|
||||
defineEmits(['update:modelValue']) |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.date-indicator-tabs { |
||||
display: flex; |
||||
align-items: flex-end; |
||||
gap: 0; |
||||
} |
||||
|
||||
.date-indicator-tab { |
||||
position: relative; |
||||
width: 78px; |
||||
height: 31px; |
||||
padding: 0; |
||||
border: none; |
||||
background: transparent; |
||||
overflow: visible; |
||||
cursor: pointer; |
||||
flex-shrink: 0; |
||||
} |
||||
|
||||
.date-indicator-tab::before { |
||||
content: ''; |
||||
position: absolute; |
||||
left: 0; |
||||
top: 0; |
||||
width: 78px; |
||||
height: 31px; |
||||
background: url('/images/日期指标.png') center / 100% 100% no-repeat; |
||||
pointer-events: none; |
||||
} |
||||
|
||||
.date-indicator-tab.active::before { |
||||
background-image: url('/images/日期指标选中.png'); |
||||
} |
||||
|
||||
.tab-text { |
||||
position: absolute; |
||||
left: 50%; |
||||
top: -3px; |
||||
transform: translateX(-50%); |
||||
font-family: 'Microsoft YaHei', sans-serif; |
||||
font-size: 16px; |
||||
font-weight: 400; |
||||
line-height: 1; |
||||
color: #ffffff; |
||||
white-space: nowrap; |
||||
pointer-events: none; |
||||
} |
||||
|
||||
.date-indicator-tab.active .tab-text { |
||||
font-size: 17px; |
||||
font-weight: 700; |
||||
color: #fff0c8; |
||||
text-shadow: 0 0 8px rgba(255, 200, 80, 0.55); |
||||
} |
||||
|
||||
.date-indicator-tab:hover::before { |
||||
filter: brightness(1.06); |
||||
} |
||||
</style> |
||||
@ -0,0 +1,115 @@ |
||||
<template> |
||||
<div class="header-section" :style="headerStyle"> |
||||
<div class="header-info left"> |
||||
<slot name="left"> |
||||
<HeaderDateTime /> |
||||
</slot> |
||||
</div> |
||||
|
||||
<div class="header-title"> |
||||
<img |
||||
v-if="isImageTitle" |
||||
:src="title" |
||||
alt="页面标题" |
||||
class="title-image" |
||||
/> |
||||
<h1 v-else class="main-title">{{ displayTitle }}</h1> |
||||
</div> |
||||
|
||||
<div class="header-info right"> |
||||
<slot name="right" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { computed } from 'vue' |
||||
import HeaderDateTime from './HeaderDateTime.vue' |
||||
|
||||
const props = defineProps({ |
||||
title: { |
||||
type: String, |
||||
default: '智慧活畜交易大数据中心' |
||||
}, |
||||
titleBackground: { |
||||
type: String, |
||||
default: '/images/标题背景.png' |
||||
} |
||||
}) |
||||
|
||||
const isImageTitle = computed(() => props.title?.startsWith('/')) |
||||
|
||||
const displayTitle = computed(() => props.title || '智慧活畜交易大数据中心') |
||||
|
||||
const headerStyle = computed(() => ({ |
||||
backgroundImage: `url('${props.titleBackground}')` |
||||
})) |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.header-section { |
||||
width: 100%; |
||||
height: 96px; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 0 40px; |
||||
background-position: center center; |
||||
background-repeat: no-repeat; |
||||
background-size: 100% 100%; |
||||
border-radius: 12px; |
||||
position: relative; |
||||
z-index: 10; |
||||
flex-shrink: 0; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
.header-title { |
||||
flex: 2; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
min-width: 0; |
||||
height: 100%; |
||||
padding: 8px 0; |
||||
} |
||||
|
||||
.title-image { |
||||
max-width: min(1200px, 70%); |
||||
max-height: 72px; |
||||
width: auto; |
||||
height: auto; |
||||
object-fit: contain; |
||||
display: block; |
||||
} |
||||
|
||||
.main-title { |
||||
margin: 0; |
||||
font-size: 36px; |
||||
font-weight: 700; |
||||
background: linear-gradient(180deg, #ffffff 0%, #ffffff 50%, #44c1ff 100%); |
||||
-webkit-background-clip: text; |
||||
-webkit-text-fill-color: transparent; |
||||
background-clip: text; |
||||
text-align: center; |
||||
letter-spacing: 3px; |
||||
filter: drop-shadow(0 0 12px rgba(64, 158, 255, 0.45)); |
||||
white-space: nowrap; |
||||
} |
||||
|
||||
.header-info { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 24px; |
||||
flex: 1; |
||||
min-width: 0; |
||||
} |
||||
|
||||
.header-info.left { |
||||
justify-content: flex-start; |
||||
} |
||||
|
||||
.header-info.right { |
||||
justify-content: flex-end; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,68 @@ |
||||
<template> |
||||
<div class="header-datetime"> |
||||
<span class="time">{{ timeText }}</span> |
||||
<span class="date-info">{{ weekdayText }} | {{ dateText }}</span> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { ref, onMounted, onUnmounted } from 'vue' |
||||
|
||||
const timeText = ref('--:--:--') |
||||
const dateText = ref('----.--.--') |
||||
const weekdayText = ref('') |
||||
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] |
||||
|
||||
const pad = (value) => String(value).padStart(2, '0') |
||||
|
||||
const updateDateTime = () => { |
||||
const now = new Date() |
||||
|
||||
timeText.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` |
||||
dateText.value = `${now.getFullYear()}.${pad(now.getMonth() + 1)}.${pad(now.getDate())}` |
||||
weekdayText.value = weekdays[now.getDay()] |
||||
} |
||||
|
||||
let timer = null |
||||
|
||||
onMounted(() => { |
||||
updateDateTime() |
||||
timer = setInterval(updateDateTime, 1000) |
||||
}) |
||||
|
||||
onUnmounted(() => { |
||||
if (timer) { |
||||
clearInterval(timer) |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.header-datetime { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 16px; |
||||
white-space: nowrap; |
||||
} |
||||
|
||||
.time { |
||||
font-family: 'Microsoft YaHei', sans-serif; |
||||
font-weight: bold; |
||||
font-size: 24px; |
||||
line-height: 1; |
||||
background: linear-gradient(0deg, #00d4ff 0%, #b8f0ff 42%, #ffffff 100%); |
||||
-webkit-background-clip: text; |
||||
background-clip: text; |
||||
-webkit-text-fill-color: transparent; |
||||
color: transparent; |
||||
} |
||||
|
||||
.date-info { |
||||
font-family: 'Microsoft YaHei', sans-serif; |
||||
font-weight: 400; |
||||
font-size: 18px; |
||||
line-height: 1; |
||||
color: #ffffff; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,31 @@ |
||||
<template> |
||||
<div class="screen-decorations-bottom" aria-hidden="true"> |
||||
<img |
||||
class="decor-bottom" |
||||
src="/images/下部.png" |
||||
alt="" |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.screen-decorations-bottom { |
||||
position: absolute; |
||||
inset: 0; |
||||
pointer-events: none; |
||||
z-index: 5; |
||||
} |
||||
|
||||
.decor-bottom { |
||||
position: absolute; |
||||
left: 50%; |
||||
bottom: 0; |
||||
transform: translateX(-50%); |
||||
width: 2196px; |
||||
height: 40px; |
||||
display: block; |
||||
object-fit: fill; |
||||
pointer-events: none; |
||||
user-select: none; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,36 @@ |
||||
/** |
||||
* 将底座图片宽度/左偏移与 ECharts grid 绘图区对齐(左右 Y 轴之间)。 |
||||
*/ |
||||
export const syncChartBaseToGrid = (chartInstance, baseEl) => { |
||||
if (!chartInstance || !baseEl) { |
||||
return |
||||
} |
||||
|
||||
const gridModel = chartInstance.getModel()?.getComponent('grid', 0) |
||||
const rect = gridModel?.coordinateSystem?.getRect?.() |
||||
if (!rect?.width) { |
||||
return |
||||
} |
||||
|
||||
baseEl.style.width = `${Math.round(rect.width)}px` |
||||
baseEl.style.marginLeft = `${Math.round(rect.x)}px` |
||||
} |
||||
|
||||
export const bindChartBaseSync = (chartInstance, baseEl) => { |
||||
if (!chartInstance || !baseEl) { |
||||
return () => {} |
||||
} |
||||
|
||||
const sync = () => { |
||||
requestAnimationFrame(() => { |
||||
syncChartBaseToGrid(chartInstance, baseEl) |
||||
}) |
||||
} |
||||
|
||||
chartInstance.on('finished', sync) |
||||
sync() |
||||
|
||||
return () => { |
||||
chartInstance.off('finished', sync) |
||||
} |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
const API_URL = '/api/dashboard/system-config' |
||||
|
||||
const DEFAULT_SYSTEM_CONFIG = { |
||||
title: '/标题.png', |
||||
titleBackground: '/images/标题背景.png' |
||||
} |
||||
|
||||
export async function loadSystemConfig() { |
||||
try { |
||||
const response = await fetch(API_URL) |
||||
if (!response.ok) { |
||||
throw new Error(`HTTP ${response.status}`) |
||||
} |
||||
|
||||
const result = await response.json() |
||||
if (result.code !== 1 || !result.data) { |
||||
throw new Error(result.message || '系统配置接口返回异常') |
||||
} |
||||
|
||||
return { |
||||
...DEFAULT_SYSTEM_CONFIG, |
||||
...result.data |
||||
} |
||||
} catch (error) { |
||||
console.warn('加载系统配置失败,使用默认配置:', error) |
||||
return { ...DEFAULT_SYSTEM_CONFIG } |
||||
} |
||||
} |
||||
|
||||
export function isImageTitle(title) { |
||||
return typeof title === 'string' && title.startsWith('/') |
||||
} |
||||