Swanky 6 months ago
parent 5e40a11ce1
commit dc5658513c
  1. 46
      package-lock.json
  2. 14
      package.json
  3. 11
      public/tv86-announcements.json
  4. 1
      src/App.vue
  5. 12
      src/RouterApp.vue
  6. 52
      src/components/BaseCard.vue
  7. 2
      src/components/SupplyDemandData.vue
  8. 264
      src/components/TV86/MarketAnnouncements.vue
  9. 613
      src/components/TV86/PriceTrendChart.vue
  10. 225
      src/components/TV86/YakTradingOverview.vue
  11. 5
      src/main.js
  12. 23
      src/router/index.js
  13. 54
      src/styles/index.scss
  14. 469
      src/views/Dashboard.vue
  15. 287
      src/views/TV86Display.vue

46
package-lock.json generated

@ -11,7 +11,9 @@
"@vueuse/core": "^10.4.1", "@vueuse/core": "^10.4.1",
"axios": "^1.5.0", "axios": "^1.5.0",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"vue": "^3.3.4" "lunar-javascript": "^1.7.3",
"vue": "^3.3.4",
"vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
@ -778,6 +780,11 @@
"@vue/shared": "3.5.16" "@vue/shared": "3.5.16"
} }
}, },
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.16", "version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz",
@ -1304,6 +1311,11 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/lunar-javascript": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/lunar-javascript/-/lunar-javascript-1.7.3.tgz",
"integrity": "sha512-p03Nj1D50UIHF66nRszKh9jDGf1nSsGlRFavrbqV6cz9UgOAcjQw7m0NRprjxPV3JsKKwhw4almHhe8BKkNFgg=="
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -1577,6 +1589,20 @@
} }
} }
}, },
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/zrender": { "node_modules/zrender": {
"version": "5.6.1", "version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
@ -1948,6 +1974,11 @@
"@vue/shared": "3.5.16" "@vue/shared": "3.5.16"
} }
}, },
"@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"@vue/reactivity": { "@vue/reactivity": {
"version": "3.5.16", "version": "3.5.16",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz",
@ -2307,6 +2338,11 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"lunar-javascript": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/lunar-javascript/-/lunar-javascript-1.7.3.tgz",
"integrity": "sha512-p03Nj1D50UIHF66nRszKh9jDGf1nSsGlRFavrbqV6cz9UgOAcjQw7m0NRprjxPV3JsKKwhw4almHhe8BKkNFgg=="
},
"magic-string": { "magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -2454,6 +2490,14 @@
"@vue/shared": "3.5.16" "@vue/shared": "3.5.16"
} }
}, },
"vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"requires": {
"@vue/devtools-api": "^6.6.4"
}
},
"zrender": { "zrender": {
"version": "5.6.1", "version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",

@ -9,14 +9,16 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"vue": "^3.3.4", "@vueuse/core": "^10.4.1",
"echarts": "^5.4.3",
"axios": "^1.5.0", "axios": "^1.5.0",
"@vueuse/core": "^10.4.1" "echarts": "^5.4.3",
"lunar-javascript": "^1.7.3",
"vue": "^3.3.4",
"vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.4.0", "@vitejs/plugin-vue": "^4.4.0",
"vite": "^4.4.9", "sass": "^1.66.1",
"sass": "^1.66.1" "vite": "^4.4.9"
} }
} }

@ -0,0 +1,11 @@
{
"announcements": [
{
"id": 1,
"content": "各位牧民、客商请注意,交易时请保管好财物,遵守市场秩序,公平议价,禁止强买强卖;运输牦牛需确保车辆牢固,避免牲畜受惊。遇纠纷可联系市场管理方协调,共同维护安全、诚信的交易环境。",
"content_tibetan": "འབག་པ་དང་ཚང་པ་ཚས་གཟགས་ནས། ཚང་འདས་སབས་རང་ག་དངལ་རས་ལགས་པར་སང་སབ་བད་པ་དང་། ཚང་འདས་ཀ་རམ་པ་བསང་བ། འགག་པའ་གང་ག་གང་སགས་བད་པ། ཁས་ལན་འདད་སལ་བད་ར་བཀག་འགག་བད་དགས། གཡག་འདན་སབས་མ་ཊར་བརན་པ་ཡད་དགས་པ་དང་། སམས་ཅན་སག་པ་སད་ར་ཚར་གཅད་བད་དགས། རད་རག་བང་ཚ་ཚང་འདས་འཛན་སང་བ་དང་རས་ལན་གང་སང་བད་ཆག ཉམས་ལན་གས་བད་འཇགས་དང་ཡད་ཆས་ཀ་ཚང་འདས་ཁར་ཡག་ཡར་རས་གཏང་བ།",
"important": true,
"created_at": "2025-01-13"
}
]
}

@ -210,7 +210,6 @@ onUnmounted(() => {
padding: 0 40px; padding: 0 40px;
background: url('./images/大标题背景.png') center/contain no-repeat; background: url('./images/大标题背景.png') center/contain no-repeat;
background-size: auto 84px; background-size: auto 84px;
border: 1px solid rgba(64, 158, 255, 0.3);
border-radius: 12px; border-radius: 12px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
position: relative; position: relative;

@ -0,0 +1,12 @@
<template>
<router-view />
</template>
<script setup>
//
</script>
<style>
/* 路由入口样式重置 */
/* 注意:不要在这里设置overflow,让各页面自己控制 */
</style>

@ -18,15 +18,22 @@ defineProps({
<style scoped> <style scoped>
.chart-card { .chart-card {
background: linear-gradient(135deg, #1a1f2e 0%, #16213e 100%); background: rgba(15, 25, 45, 0.3);
border-radius: 8px; backdrop-filter: blur(20px) saturate(180%);
padding: 16px; -webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(64, 158, 255, 0.3); border-radius: 12px;
height: 100%; padding: 18px;
margin: 6px;
height: calc(100% - 12px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
0 4px 16px rgba(64, 158, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.chart-card::before { .chart-card::before {
@ -35,11 +42,23 @@ defineProps({
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 1px;
background: linear-gradient(90deg, #409EFF, #67C23A, #E6A23C); background: linear-gradient(90deg, transparent 0%, rgba(64, 158, 255, 0.5) 50%, transparent 100%);
opacity: 0.8; opacity: 0.8;
} }
.chart-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, transparent 50%, rgba(64, 158, 255, 0.03) 100%);
pointer-events: none;
opacity: 0.6;
}
.card-title { .card-title {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
@ -50,7 +69,8 @@ defineProps({
margin-bottom: 12px; margin-bottom: 12px;
text-align: left; text-align: left;
position: relative; position: relative;
z-index: 1; z-index: 2;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
.card-content { .card-content {
@ -58,13 +78,19 @@ defineProps({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
z-index: 1; z-index: 2;
} }
.chart-card:hover { .chart-card:hover {
border-color: rgba(64, 158, 255, 0.6); transform: translateY(-2px) scale(1.02);
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2); box-shadow:
transform: translateY(-2px); 0 12px 48px rgba(0, 0, 0, 0.4),
transition: all 0.3s ease; 0 8px 24px rgba(64, 158, 255, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
background: rgba(15, 25, 45, 0.4);
}
.chart-card:hover::after {
opacity: 0.8;
} }
</style> </style>

@ -410,7 +410,7 @@ onMounted(async () => {
.supply-info-content.expanded .table-header, .supply-info-content.expanded .table-header,
.supply-info-content.expanded .table-row { .supply-info-content.expanded .table-row {
grid-template-columns: 140px 140px 100px 160px 160px 140px 160px 140px 100px; grid-template-columns: 1fr 1fr 0.8fr 1fr 1fr 1fr 1.2fr 1fr 0.6fr;
} }
.supply-info-content.expanded .header-cell, .supply-info-content.expanded .header-cell,

@ -0,0 +1,264 @@
<template>
<div class="announcements-card">
<div class="card-header">
<div class="title-container">
<h2 class="card-title">市场公告</h2>
<div class="title-tibetan">འདབར</div>
</div>
</div>
<div class="announcements-content">
<div v-if="loading" class="loading-message">
<div class="loading-spinner"></div>
<div>正在加载公告内容...</div>
</div>
<div v-else-if="error" class="error-message">
<div>{{ error }}</div>
</div>
<div v-else class="announcement-list">
<div
v-for="announcement in announcements"
:key="announcement.id"
class="announcement-item"
:class="{ important: announcement.important }"
>
<div class="announcement-content-chinese">
{{ announcement.content }}
</div>
<div class="announcement-content-tibetan">
{{ announcement.content_tibetan }}
</div>
<!-- <div v-if="announcement.important" class="important-badge">
重要公告
</div> -->
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const announcements = ref([])
const loading = ref(true)
const error = ref('')
// JSON
const loadAnnouncements = async () => {
try {
loading.value = true
error.value = ''
const response = await fetch('/tv86-announcements.json')
if (!response.ok) {
throw new Error('无法加载公告内容')
}
const data = await response.json()
announcements.value = data.announcements || []
console.log('公告内容加载成功:', announcements.value)
} catch (err) {
console.error('加载公告失败:', err)
error.value = '加载公告内容失败,请稍后重试'
// 使
announcements.value = [
{
id: 1,
content: '各位牧民、客商请注意,交易时请保管好财物,遵守市场秩序,公平议价,禁止强买强卖;运输牦牛需确保车辆牢固,避免牲畜受惊。遇纠纷可联系市场管理方协调,共同维护安全、诚信的交易环境。',
content_tibetan: 'འབག་པ་དང་ཚང་པ་ཚས་གཟགས་ནས། ཚང་འདས་སབས་རང་ག་དངལ་རས་ལགས་པར་སང་སབ་བད་པ་དང་། ཚང་འདས་ཀ་རམ་པ་བསང་བ། འགག་པའ་གང་ག་གང་སགས་བད་པ། ཁས་ལན་འདད་སལ་བད་ར་བཀག་འགག་བད་དགས། གཡག་འདན་སབས་མ་ཊར་བརན་པ་ཡད་དགས་པ་དང་། སམས་ཅན་སག་པ་སད་ར་ཚར་གཅད་བད་དགས། རད་རག་བང་ཚ་ཚང་འདས་འཛན་སང་བ་དང་རས་ལན་གང་སང་བད་ཆག ཉམས་ལན་གས་བད་འཇགས་དང་ཡད་ཆས་ཀ་ཚང་འདས་ཁར་ཡག་ཡར་རས་གཏང་བ།',
important: true,
created_at: '2025-01-13'
}
]
} finally {
loading.value = false
}
}
onMounted(() => {
//
loadAnnouncements()
// 5
setInterval(() => {
loadAnnouncements()
}, 300000) // 5
})
</script>
<style scoped>
.announcements-card {
height: 100%;
background: rgba(15, 25, 45, 0.4);
backdrop-filter: blur(20px) saturate(180%);
border-radius: 32px;
padding: 48px;
display: flex;
flex-direction: column;
box-shadow:
0 16px 64px rgba(0, 0, 0, 0.3),
0 8px 32px rgba(64, 158, 255, 0.1),
inset 0 2px 0 rgba(255, 255, 255, 0.1);
}
.card-header {
margin-bottom: 40px;
}
.title-container {
display: flex;
align-items: center;
gap: 32px;
}
.card-title {
font-size: 48px;
font-weight: 700;
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
text-align: left;
}
.title-tibetan {
font-size: 36px;
color: #67C23A;
font-weight: 600;
opacity: 0.8;
}
.announcements-content {
flex: 1;
overflow: hidden;
}
.announcement-list {
height: 100%;
overflow-y: auto;
padding-right: 8px;
}
.announcement-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 24px;
padding: 48px;
margin-bottom: 40px;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
transition: all 0.3s ease;
}
.announcement-item:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.1);
}
.announcement-item.important {
border-color: rgba(245, 108, 108, 0.4);
background: rgba(245, 108, 108, 0.08);
}
.announcement-content-chinese {
font-size: 42px;
line-height: 1.8;
color: #ffffff;
text-align: justify;
margin-bottom: 32px;
font-weight: 500;
}
.announcement-content-tibetan {
font-size: 42px;
line-height: 1.8;
color: #67C23A;
text-align: justify;
opacity: 0.9;
font-weight: 500;
}
.important-badge {
position: absolute;
top: 24px;
right: 24px;
background: linear-gradient(135deg, #F56C6C, #E6A23C);
color: #ffffff;
font-size: 24px;
font-weight: 600;
padding: 8px 16px;
border-radius: 24px;
box-shadow: 0 4px 16px rgba(245, 108, 108, 0.3);
}
/* 加载和错误状态样式 */
.loading-message, .error-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #a0a8b8;
font-size: 32px;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(64, 158, 255, 0.3);
border-top: 4px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24px;
}
.error-message {
color: #F56C6C;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 滚动条样式 */
.announcement-list::-webkit-scrollbar {
width: 12px;
}
.announcement-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
.announcement-list::-webkit-scrollbar-thumb {
background: rgba(64, 158, 255, 0.5);
border-radius: 6px;
}
.announcement-list::-webkit-scrollbar-thumb:hover {
background: rgba(64, 158, 255, 0.7);
}
/* 动画效果 */
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.announcement-item {
animation: slideIn 0.5s ease-out;
}
</style>

@ -0,0 +1,613 @@
<template>
<div class="price-trend-card">
<div class="card-header">
<div class="title-container">
<h2 class="card-title">
活牛鲜肉价格趋势
<span class="title-tibetan">ཆགསདངའག</span>
</h2>
</div>
<div class="controls-container">
<div class="period-buttons">
<button
v-for="(period, index) in periodTypes"
:key="period"
:class="['period-btn', { active: currentPeriodIndex === index }]"
@click="switchToPeriod(index)"
>
<div class="btn-text">{{ priceData[period]?.title || '加载中' }}</div>
<div class="btn-text-tibetan">{{ priceData[period]?.title_tibetan || '' }}</div>
</button>
</div>
<div class="auto-switch-control">
<!-- <button
:class="['auto-btn', { active: autoSwitch }]"
@click="toggleAutoSwitch"
>
<div class="btn-text">{{ autoSwitch ? '自动切换中' : '手动模式' }}</div>
<div class="btn-text-tibetan">{{ autoSwitch ? 'རང་འགལ་བར་བ།' : 'ལག་རས་བད་སད།' }}</div>
</button> -->
</div>
</div>
</div>
<div v-if="loading" class="loading-message">
<div class="loading-spinner"></div>
<div>正在加载价格数据...</div>
</div>
<div v-else-if="error" class="error-message">
<div>{{ error }}</div>
</div>
<div v-else class="chart-container">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref(null)
let chartInstance = null
let switchTimer = null
//
const priceData = ref({})
const loading = ref(true)
const error = ref('')
const currentPeriodType = ref('daily')
const currentPeriod = ref({ title: '日统计', title_tibetan: 'ཉན་ར་གང་རས།' })
const autoSwitch = ref(true)
//
const periodTypes = ['daily', 'weekly', 'monthly']
const currentPeriodIndex = ref(0)
//
const generateDynamicData = () => {
const now = new Date()
// - 10
const dailyDates = []
const dailyLiveCattle = []
const dailyFreshMeat = []
for (let i = 9; i >= 0; i--) {
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
dailyDates.push(`${date.getMonth() + 1}/${date.getDate()}`)
// /
dailyLiveCattle.push(Math.round((28 + Math.sin(i * 0.5) * 2 + Math.cos(i * 0.3) * 1.5) * 10) / 10)
dailyFreshMeat.push(Math.round((76.5 + Math.sin(i * 0.4) * 1.2 + Math.cos(i * 0.6) * 0.8) * 10) / 10)
}
// - 10
const weeklyDates = []
const weeklyLiveCattle = []
const weeklyFreshMeat = []
for (let i = 9; i >= 0; i--) {
const startDate = new Date(now.getTime() - i * 7 * 24 * 60 * 60 * 1000)
const endDate = new Date(startDate.getTime() + 6 * 24 * 60 * 60 * 1000)
//
const startMonth = startDate.getMonth() + 1
const startDay = startDate.getDate()
const endMonth = endDate.getMonth() + 1
const endDay = endDate.getDate()
if (startMonth === endMonth) {
weeklyDates.push(`${startMonth}/${startDay}-${endDay}`)
} else {
weeklyDates.push(`${startMonth}/${startDay}-${endMonth}/${endDay}`)
}
// /
weeklyLiveCattle.push(Math.round((29.5 + Math.sin(i * 0.6) * 2.5 + Math.cos(i * 0.4) * 2.0) * 10) / 10)
weeklyFreshMeat.push(Math.round((78.8 + Math.sin(i * 0.7) * 2.0 + Math.cos(i * 0.5) * 1.5) * 10) / 10)
}
// - 12
const monthlyDates = []
const monthlyLiveCattle = []
const monthlyFreshMeat = []
for (let i = 11; i >= 0; i--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - i, 1)
const year = monthDate.getFullYear()
const month = monthDate.getMonth() + 1
monthlyDates.push(`${year}${month}`)
// /
monthlyLiveCattle.push(Math.round((27.5 + Math.sin(i * 0.8) * 3.5 + Math.cos(i * 0.6) * 2.8) * 10) / 10)
monthlyFreshMeat.push(Math.round((76.0 + Math.sin(i * 0.9) * 3.0 + Math.cos(i * 0.7) * 2.5) * 10) / 10)
}
return {
daily: {
title: '日统计',
title_tibetan: 'ཉན་ར་གང་རས།',
dates: dailyDates,
liveCattlePrice: dailyLiveCattle,
freshMeatPrice: dailyFreshMeat
},
weekly: {
title: '周统计',
title_tibetan: 'གཟའ་འཁར་གང་རས།',
dates: weeklyDates,
liveCattlePrice: weeklyLiveCattle,
freshMeatPrice: weeklyFreshMeat
},
monthly: {
title: '月统计',
title_tibetan: 'ཟ་བའ་གང་རས།',
dates: monthlyDates,
liveCattlePrice: monthlyLiveCattle,
freshMeatPrice: monthlyFreshMeat
}
}
}
// JSON
const loadPriceData = async () => {
try {
loading.value = true
error.value = ''
// 使
priceData.value = generateDynamicData()
//
updateCurrentPeriod()
console.log('价格数据生成成功:', priceData.value)
} catch (err) {
console.error('生成价格数据失败:', err)
error.value = '生成价格数据失败,请稍后重试'
// 使
const fallbackDate = new Date()
priceData.value = {
daily: {
title: '日统计',
title_tibetan: 'ཉན་ར་གང་རས།',
dates: [`${fallbackDate.getMonth() + 1}/${fallbackDate.getDate()}`],
liveCattlePrice: [5600],
freshMeatPrice: [76.5]
}
}
updateCurrentPeriod()
} finally {
loading.value = false
}
}
//
const updateCurrentPeriod = () => {
const periodType = periodTypes[currentPeriodIndex.value]
currentPeriodType.value = periodType
if (priceData.value[periodType]) {
currentPeriod.value = {
title: priceData.value[periodType].title,
title_tibetan: priceData.value[periodType].title_tibetan
}
//
if (chartInstance && !loading.value) {
initChart()
}
}
}
//
const switchPeriod = () => {
currentPeriodIndex.value = (currentPeriodIndex.value + 1) % periodTypes.length
updateCurrentPeriod()
}
//
const switchToPeriod = (index) => {
currentPeriodIndex.value = index
updateCurrentPeriod()
console.log(`手动切换到: ${periodTypes[index]}`)
}
// /
const toggleAutoSwitch = () => {
autoSwitch.value = !autoSwitch.value
if (autoSwitch.value) {
//
if (switchTimer) {
clearInterval(switchTimer)
}
switchTimer = setInterval(() => {
switchPeriod()
}, 10000)
console.log('启动自动切换模式')
} else {
//
if (switchTimer) {
clearInterval(switchTimer)
switchTimer = null
}
console.log('切换到手动模式')
}
}
const initChart = () => {
if (!chartRef.value || loading.value) return
const currentData = priceData.value[currentPeriodType.value]
if (!currentData) return
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value)
}
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(15, 25, 45, 0.95)',
borderColor: '#409EFF',
borderWidth: 2,
textStyle: {
color: '#fff',
fontSize: 36
},
formatter: function(params) {
let result = `<div style="padding: 20px;">`
result += `<strong style="color: #409EFF; font-size: 40px;">${params[0].axisValue}</strong><br/>`
params.forEach(param => {
result += `<span style="color: ${param.color}; font-size: 36px;">● ${param.seriesName}: ${param.value.toFixed(param.seriesName.includes('活牛') ? 1 : 1)}元/斤</span><br/>`
})
result += `</div>`
return result
}
},
legend: {
data: ['活牛价格', '鲜肉价格'],
top: '8%',
textStyle: {
color: '#a0a8b8',
fontSize: 40
},
itemWidth: 50,
itemHeight: 35
},
grid: {
left: '10%',
right: '10%',
bottom: '18%',
top: '25%',
containLabel: true
},
xAxis: {
type: 'category',
data: currentData.dates,
axisLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.3)',
width: 2
}
},
axisLabel: {
color: '#a0a8b8',
fontSize: 32
},
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
name: '价格(元/斤)',
nameTextStyle: {
color: '#ffffff',
fontSize: 36,
padding: [0, 0, 0, 20]
},
position: 'left',
axisLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.3)',
width: 2
}
},
axisLabel: {
color: '#a0a8b8',
fontSize: 32,
formatter: '{value}'
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)',
width: 1
}
}
},
series: [
{
name: '活牛价格',
type: 'line',
data: currentData.liveCattlePrice,
smooth: true,
symbol: 'circle',
symbolSize: 12,
lineStyle: {
color: '#409EFF',
width: 6
},
itemStyle: {
color: '#409EFF'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
]
}
}
},
{
name: '鲜肉价格',
type: 'line',
data: currentData.freshMeatPrice,
smooth: true,
symbol: 'circle',
symbolSize: 12,
lineStyle: {
color: '#67C23A',
width: 6
},
itemStyle: {
color: '#67C23A'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
{ offset: 1, color: 'rgba(103, 194, 58, 0.05)' }
]
}
}
}
]
}
chartInstance.setOption(option, true)
}
onMounted(async () => {
//
await loadPriceData()
//
initChart()
//
window.addEventListener('resize', () => {
chartInstance?.resize()
})
// autoSwitch
if (autoSwitch.value) {
switchTimer = setInterval(() => {
switchPeriod()
}, 10000)
console.log('PriceTrendChart组件已加载,自动切换模式已启动')
} else {
console.log('PriceTrendChart组件已加载,手动模式')
}
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
if (switchTimer) {
clearInterval(switchTimer)
switchTimer = null
}
window.removeEventListener('resize', () => {
chartInstance?.resize()
})
})
</script>
<style scoped>
.price-trend-card {
height: 100%;
background: rgba(15, 25, 45, 0.4);
backdrop-filter: blur(20px) saturate(180%);
border-radius: 32px;
padding: 48px;
display: flex;
flex-direction: column;
box-shadow:
0 16px 64px rgba(0, 0, 0, 0.3),
0 8px 32px rgba(64, 158, 255, 0.1),
inset 0 2px 0 rgba(255, 255, 255, 0.1);
}
.card-header {
margin-bottom: 32px;
}
.title-container {
width: 100%;
}
.card-title {
font-size: 48px;
font-weight: 700;
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
text-align: left;
line-height: 1.2;
}
.title-tibetan {
font-size: 36px;
font-weight: 600;
color: #a0a8b8;
margin-left: 16px;
display: inline;
white-space: nowrap;
}
.loading-message {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
color: #a0a8b8;
font-size: 32px;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(64, 158, 255, 0.2);
border-top: 4px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
.error-message {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #F56C6C;
font-size: 32px;
text-align: center;
}
.chart-container {
flex: 1;
min-height: 0;
}
.chart {
width: 100%;
height: 100%;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.controls-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
gap: 32px;
}
.period-buttons {
display: flex;
gap: 16px;
}
.period-btn {
background: rgba(64, 158, 255, 0.1);
border: 2px solid rgba(64, 158, 255, 0.3);
border-radius: 16px;
padding: 16px 32px;
cursor: pointer;
transition: all 0.3s ease;
color: #a0a8b8;
font-size: 32px;
min-width: 160px;
}
.period-btn:hover {
background: rgba(64, 158, 255, 0.2);
border-color: rgba(64, 158, 255, 0.5);
transform: translateY(-2px);
}
.period-btn.active {
background: rgba(64, 158, 255, 0.3);
border-color: #409EFF;
color: #409EFF;
box-shadow: 0 8px 24px rgba(64, 158, 255, 0.3);
}
.period-btn .btn-text {
font-weight: 600;
margin-bottom: 4px;
}
.period-btn .btn-text-tibetan {
font-size: 28px;
opacity: 0.8;
}
.auto-switch-control {
margin-left: auto;
}
.auto-btn {
background: rgba(103, 194, 58, 0.1);
border: 2px solid rgba(103, 194, 58, 0.3);
border-radius: 16px;
padding: 16px 32px;
cursor: pointer;
transition: all 0.3s ease;
color: #a0a8b8;
font-size: 32px;
min-width: 200px;
}
.auto-btn:hover {
background: rgba(103, 194, 58, 0.2);
border-color: rgba(103, 194, 58, 0.5);
transform: translateY(-2px);
}
.auto-btn.active {
background: rgba(103, 194, 58, 0.3);
border-color: #67C23A;
color: #67C23A;
box-shadow: 0 8px 24px rgba(103, 194, 58, 0.3);
}
.auto-btn .btn-text {
font-weight: 600;
margin-bottom: 4px;
}
.auto-btn .btn-text-tibetan {
font-size: 28px;
opacity: 0.8;
}
</style>

@ -0,0 +1,225 @@
<template>
<div class="trading-overview-card">
<div class="card-header">
<div class="title-container">
<h2 class="card-title">牦牛实时交易概况</h2>
<div class="title-tibetan">འབགཡགའདགནསངས</div>
</div>
</div>
<div class="overview-content">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">
<span class="chinese">今日入场牦牛数</span>
<span class="tibetan">གསཔའགཡགངས</span>
</div>
<div class="stat-value primary">{{ todayEntryYaks }}</div>
<div class="stat-unit"></div>
</div>
<div class="stat-item">
<div class="stat-label">
<span class="chinese">已交易牦牛数</span>
<span class="tibetan">པའགཡགངས</span>
</div>
<div class="stat-value success">{{ tradedYaks }}</div>
<div class="stat-unit"></div>
</div>
<div class="stat-item">
<div class="stat-label">
<span class="chinese">待交易牦牛数</span>
<span class="tibetan">བསཔའགཡགངས</span>
</div>
<div class="stat-value warning">{{ waitingYaks }}</div>
<div class="stat-unit"></div>
</div>
<div class="stat-item">
<div class="stat-label">
<span class="chinese">卖家人数</span>
<span class="tibetan">པའངས</span>
</div>
<div class="stat-value info">{{ sellerCount }}</div>
<div class="stat-unit"></div>
</div>
<div class="stat-item">
<div class="stat-label">
<span class="chinese">剩余车位</span>
<span class="tibetan">པའགནསསའངས</span>
</div>
<div class="stat-value danger">{{ remainingParkingSpots }}</div>
<div class="stat-unit"></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
//
const todayEntryYaks = ref(1256) //
const tradedYaks = ref(847) //
const waitingYaks = ref(409) //
const sellerCount = ref(168) //
const remainingParkingSpots = ref(32) //
//
onMounted(() => {
setInterval(() => {
//
const newEntry = Math.floor(Math.random() * 5)
todayEntryYaks.value += newEntry
//
const newTrades = Math.floor(Math.random() * 3)
if (waitingYaks.value >= newTrades) {
tradedYaks.value += newTrades
waitingYaks.value -= newTrades
}
// -
waitingYaks.value += (newEntry - newTrades)
if (waitingYaks.value < 0) waitingYaks.value = 0
//
if (Math.random() > 0.7) {
sellerCount.value += Math.floor(Math.random() * 3) - 1 // -11
remainingParkingSpots.value += Math.floor(Math.random() * 5) - 2 // -22
//
if (sellerCount.value < 100) sellerCount.value = 100
if (sellerCount.value > 300) sellerCount.value = 300
if (remainingParkingSpots.value < 0) remainingParkingSpots.value = 0
if (remainingParkingSpots.value > 50) remainingParkingSpots.value = 50
}
}, 30000) // 30
})
</script>
<style scoped>
.trading-overview-card {
height: 100%;
background: rgba(15, 25, 45, 0.4);
backdrop-filter: blur(20px) saturate(180%);
border-radius: 32px;
padding: 48px;
display: flex;
flex-direction: column;
box-shadow:
0 16px 64px rgba(0, 0, 0, 0.3),
0 8px 32px rgba(64, 158, 255, 0.1),
inset 0 2px 0 rgba(255, 255, 255, 0.1);
}
.card-header {
margin-bottom: 40px;
}
.title-container {
display: flex;
align-items: center;
gap: 32px;
}
.card-title {
font-size: 48px;
font-weight: 700;
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
text-align: left;
}
.title-tibetan {
font-size: 36px;
color: #67C23A;
font-weight: 600;
opacity: 0.8;
}
.overview-content {
flex: 1;
display: flex;
flex-direction: column;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
flex: 1;
}
.stat-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 24px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
justify-content: center;
min-height: 0;
}
.stat-label {
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.stat-label .chinese {
font-size: 36px;
color: #ffffff;
font-weight: 600;
line-height: 1.2;
}
.stat-label .tibetan {
font-size: 36px;
color: #67C23A;
opacity: 0.8;
line-height: 1.2;
}
.stat-value {
font-size: 64px;
font-weight: 700;
margin-bottom: 8px;
line-height: 1;
}
.stat-value.primary {
color: #409EFF;
}
.stat-value.success {
color: #67C23A;
}
.stat-value.warning {
color: #E6A23C;
}
.stat-value.danger {
color: #F56C6C;
}
.stat-value.info {
color: #17A2B8;
}
.stat-unit {
font-size: 32px;
color: #a0a8b8;
margin-top: 8px;
}
</style>

@ -1,5 +1,6 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import RouterApp from './RouterApp.vue'
import router from './router'
import './styles/index.scss' import './styles/index.scss'
createApp(App).mount('#app') createApp(RouterApp).use(router).mount('#app')

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
import TV86Display from '../views/TV86Display.vue'
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard
},
{
path: '/tv86',
name: 'TV86Display',
component: TV86Display
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

@ -1,4 +1,4 @@
// CSS变量定义 // CSS变量定义 - 这些变量会被JavaScript动态设置
:root { :root {
--screen-width: 5120px; --screen-width: 5120px;
--screen-height: 1440px; --screen-height: 1440px;
@ -23,39 +23,20 @@ html, body {
} }
#app { #app {
width: var(--screen-width); width: 100%;
height: var(--screen-height); height: 100%;
min-width: var(--screen-width);
min-height: var(--screen-height);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
} }
// 固定尺寸布局 - 使用flex布局 // 全局背景动画
.dashboard-container { @keyframes backgroundShift {
width: var(--screen-width); 0%, 100% {
height: var(--screen-height); opacity: 1;
display: flex; }
flex-direction: column; 50% {
padding: 16px; opacity: 0.7;
background: radial-gradient(ellipse at center, rgba(64, 158, 255, 0.1) 0%, $bg-primary 70%);
overflow: hidden;
box-sizing: border-box;
gap: 16px;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(90deg, transparent 0%, rgba(64, 158, 255, 0.02) 50%, transparent 100%),
linear-gradient(0deg, transparent 0%, rgba(139, 92, 246, 0.02) 50%, transparent 100%);
pointer-events: none;
z-index: 1;
} }
} }
@ -150,17 +131,4 @@ html, body {
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
// 响应式适配 - 针对4:1宽高比优化 // 响应式适配相关样式已移至各组件内部
@media (max-width: 1919px) and (min-width: 1600px) {
.dashboard-container {
padding: 6px;
gap: 6px;
}
}
@media (max-width: 1599px) {
.dashboard-container {
padding: 5px;
gap: 5px;
}
}

@ -0,0 +1,469 @@
<template>
<div class="dashboard-container">
<!-- 标题区域 -->
<div class="header-section">
<div class="header-info left">
<!-- <div class="info-item">
<span class="status-indicator" :class="isLayoutReady ? 'online' : 'loading'"></span>
<span>{{ isLayoutReady ? '系统运行正常' : '系统初始化中' }}</span>
</div> -->
</div>
<h1 class="main-title">{{ config?.screen?.title || '智慧活畜交易大数据中心' }}</h1>
<div class="header-info right">
<div class="info-item">
<span class="current-time">{{ currentTime || '--:--:--' }}</span>
</div>
</div>
</div>
<!-- 主要内容区域 -->
<div v-if="isLayoutReady" class="main-content">
<!-- 左侧区域 - 分为左和左中两列 -->
<div class="left-section">
<!-- 左列 -->
<div class="left-column">
<div class="left-top">
<RealTimeStats />
</div>
<div class="left-bottom">
<YakTradingData />
</div>
</div>
<!-- 左中列 -->
<div class="left-center-column">
<div class="left-center-top">
<ComprehensiveSalesStats />
</div>
<div class="left-center-bottom">
<YakPriceTrend />
</div>
</div>
</div>
<!-- 中央区域 -->
<div class="center-section">
<div class="map-container">
<ChinaMap />
<!-- <ScrollingAnnouncement /> -->
<!-- <div class="floating-table">
<TransactionDetails />
</div> -->
</div>
</div>
<!-- 右侧区域 - 分为右中和右两列 -->
<div class="right-section">
<!-- 当供应信息展开时整个右侧区域显示供应信息 -->
<div v-if="isSupplyExpanded" class="expanded-supply-container">
<SupplyDemandData :force-expanded="true" @expand-change="handleSupplyExpand" />
</div>
<!-- 正常状态下的右侧布局 -->
<template v-else>
<!-- 右中列 -->
<div class="right-center-column">
<div class="right-center-top">
<ExchangeMonitor />
</div>
<div class="right-center-middle">
<YakSalesTypeStats />
</div>
<div class="right-center-bottom">
<PurchaserAnalysis />
</div>
</div>
<!-- 右列 -->
<div class="right-column">
<div class="right-bottom">
<SupplyDemandData :force-expanded="false" @expand-change="handleSupplyExpand" />
</div>
<div class="right-middle">
<MarketRealtimeMonitor />
</div>
<div class="right-top">
<MarketEnvironmentMonitor />
</div>
</div>
</template>
</div>
</div>
<!-- 加载状态 -->
<div v-else class="loading-container">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text">正在初始化布局...</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { configManager } from '../utils/config.js'
import { dataManager } from '../utils/dataManager.js'
import { dataUpdater } from '../utils/dataUpdater.js'
//
import RealTimeStats from '../components/RealTimeStats.vue'
import YakTradingData from '../components/YakTradingData.vue'
import ComprehensiveSalesStats from '../components/ComprehensiveSalesStats.vue'
import YakPriceTrend from '../components/YakPriceTrend.vue'
import ChinaMap from '../components/ChinaMap.vue'
import ScrollingAnnouncement from '../components/ScrollingAnnouncement.vue'
import TransactionDetails from '../components/TransactionDetails.vue'
import ExchangeMonitor from '../components/ExchangeMonitor.vue'
import YakSalesTypeStats from '../components/YakSalesTypeStats.vue'
import PurchaserAnalysis from '../components/PurchaserAnalysis.vue'
import MarketEnvironmentMonitor from '../components/MarketEnvironmentMonitor.vue'
import MarketRealtimeMonitor from '../components/MarketRealtimeMonitor.vue'
import SupplyDemandData from '../components/SupplyDemandData.vue'
//
const config = ref(null)
const currentTime = ref('')
const isLayoutReady = ref(false) //
const isSupplyExpanded = ref(false) //
//
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// /
const handleSupplyExpand = (expanded) => {
isSupplyExpanded.value = expanded
}
//
let timeTimer = null
onMounted(async () => {
//
updateTime()
timeTimer = setInterval(updateTime, 1000)
try {
//
console.log('Dashboard: 开始加载配置')
config.value = await configManager.loadConfig()
console.log('Dashboard: 配置加载完成', config.value)
// CSS
console.log('Dashboard: 设置CSS变量')
configManager.setCSSVariables()
// CSSDOM
await nextTick()
//
await new Promise(resolve => requestAnimationFrame(resolve))
console.log('Dashboard: CSS变量已生效')
//
console.log('Dashboard: 开始加载数据配置')
const loadedData = await dataManager.loadData()
console.log('Dashboard: 数据配置加载完成', loadedData)
//
console.log('Dashboard: 布局准备完成,开始渲染组件')
isLayoutReady.value = true
console.log('Dashboard 完全初始化完成')
} catch (error) {
console.error('Failed to initialize dashboard:', error)
// 使
config.value = configManager.getDefaultConfig()
configManager.setCSSVariables()
// DOM
await nextTick()
await new Promise(resolve => requestAnimationFrame(resolve))
isLayoutReady.value = true
}
})
onUnmounted(() => {
if (timeTimer) {
clearInterval(timeTimer)
}
})
</script>
<style scoped>
/* Dashboard专用样式 - 4:1大屏布局 */
.dashboard-container {
width: var(--screen-width, 5120px);
height: var(--screen-height, 1440px);
min-width: var(--screen-width, 5120px);
min-height: var(--screen-height, 1440px);
display: flex;
flex-direction: column;
padding: 20px;
background:
radial-gradient(ellipse at 30% 40%, rgba(64, 158, 255, 0.06) 0%, transparent 60%),
radial-gradient(ellipse at 70% 60%, rgba(139, 92, 246, 0.05) 0%, transparent 60%),
radial-gradient(ellipse at 50% 80%, rgba(0, 212, 170, 0.03) 0%, transparent 50%),
linear-gradient(135deg, #0a0e1a 0%, #0f1419 25%, #131825 50%, #0f1419 75%, #0a0e1a 100%);
overflow: hidden;
box-sizing: border-box;
gap: 20px;
position: relative;
}
.dashboard-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(90deg, transparent 0%, rgba(64, 158, 255, 0.03) 50%, transparent 100%),
linear-gradient(0deg, transparent 0%, rgba(139, 92, 246, 0.03) 50%, transparent 100%),
radial-gradient(circle at 25% 25%, rgba(64, 158, 255, 0.05) 0%, transparent 50%),
radial-gradient(circle at 75% 75%, rgba(139, 92, 246, 0.05) 0%, transparent 50%);
pointer-events: none;
z-index: 1;
animation: backgroundShift 15s ease-in-out infinite;
}
@keyframes backgroundShift {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* 响应式适配 - 针对4:1宽高比优化 */
@media (max-width: 1919px) and (min-width: 1600px) {
.dashboard-container {
padding: 6px;
gap: 6px;
}
}
@media (max-width: 1599px) {
.dashboard-container {
padding: 5px;
gap: 5px;
}
}
/* 标题区域 */
.header-section {
height: 84px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 40px;
background: url('../images/大标题背景.png') center/contain no-repeat;
background-size: auto 84px;
border-radius: 12px;
backdrop-filter: blur(10px);
position: relative;
z-index: 10;
flex-shrink: 0;
background-size: 100% 84px;
}
.main-title {
font-size: 36px;
font-weight: 700;
background: linear-gradient(180deg, #fff 50%, #44c1ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
letter-spacing: 3px;
text-shadow: 0 0 20px rgba(64, 158, 255, 0.5);
flex: 2;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
}
.header-info {
display: flex;
align-items: center;
gap: 24px;
flex: 1;
&.left {
justify-content: flex-start;
}
&.right {
justify-content: flex-end;
}
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #a0a8b8;
}
.current-time {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #409EFF;
}
/* 主要内容区域 */
.main-content {
flex: 1;
display: flex;
gap: 16px;
min-height: 0;
padding-bottom: 20px;
}
/* 左侧区域布局 */
.left-section {
flex: 1;
display: flex;
gap: 12px;
min-width: 0;
}
.left-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.left-center-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.left-top,
.left-bottom,
.left-center-top,
.left-center-bottom {
flex: 1;
min-height: 0;
}
/* 中央区域布局 */
.center-section {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.map-container {
position: relative;
flex: 1;
min-height: 0;
}
.floating-table {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
height: 200px;
background: rgba(26, 31, 46, 0.95);
border: 1px solid rgba(64, 158, 255, 0.4);
border-radius: 8px;
backdrop-filter: blur(10px);
z-index: 100;
}
/* 右侧区域布局 */
.right-section {
flex: 1;
display: flex;
gap: 12px;
min-width: 0;
}
.right-center-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.right-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.right-center-top,
.right-center-middle,
.right-center-bottom,
.right-top,
.right-middle,
.right-bottom {
flex: 1;
min-height: 0;
}
/* 展开供应信息的容器 */
.expanded-supply-container {
flex: 1;
min-height: 0;
}
/* 加载状态 */
.loading-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.loading-content {
text-align: center;
color: #a0a8b8;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(64, 158, 255, 0.3);
border-top: 3px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
.loading-text {
font-size: 14px;
color: #a0a8b8;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

@ -0,0 +1,287 @@
<template>
<div class="tv86-viewport">
<div class="tv86-container" ref="containerRef">
<!-- 顶部时间日期天气区域 -->
<div class="header-datetime">
<div class="datetime-info">
<span class="date">{{ currentDate }}</span>
<span class="weekday">{{ currentWeekday }}</span>
<span class="lunar">{{ lunarDate }}</span>
<span class="weather">{{ weather }}</span>
<span class="temperature">{{ temperature }}</span>
</div>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 左侧区域 -->
<div class="left-section">
<!-- 左上牦牛实时交易概况 -->
<div class="left-top">
<YakTradingOverview />
</div>
<!-- 左下活牛鲜肉价格趋势 -->
<div class="left-bottom">
<PriceTrendChart />
</div>
</div>
<!-- 右侧市场公告 -->
<div class="right-section">
<MarketAnnouncements />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import YakTradingOverview from '../components/TV86/YakTradingOverview.vue'
import PriceTrendChart from '../components/TV86/PriceTrendChart.vue'
import MarketAnnouncements from '../components/TV86/MarketAnnouncements.vue'
import { Lunar, Solar } from 'lunar-javascript'
//
const BASE_WIDTH = 3840
const BASE_HEIGHT = 2160
//
const containerRef = ref(null)
const currentDate = ref('')
const currentWeekday = ref('')
const lunarDate = ref('')
const weather = ref('晴')
const temperature = ref('36°C')
//
const updateDateTime = () => {
const now = new Date()
//
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
currentDate.value = `${year}${month}${day}`
//
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
currentWeekday.value = weekdays[now.getDay()]
//
try {
const solar = Solar.fromDate(now)
const lunar = solar.getLunar()
//
const lunarMonth = lunar.getMonthInChinese()
const lunarDay = lunar.getDayInChinese()
// 使toString
const lunarString = lunar.toString()
const isLeapMonth = lunarString.includes('闰')
//
let monthName = lunarMonth
//
const monthMap = {
'正': '正月',
'一': '正月',
'二': '二月',
'三': '三月',
'四': '四月',
'五': '五月',
'六': '六月',
'七': '七月',
'八': '八月',
'九': '九月',
'十': '十月',
'冬': '冬月',
'腊': '腊月'
}
if (monthMap[monthName]) {
monthName = monthMap[monthName]
} else if (!monthName.includes('月')) {
monthName = monthName + '月'
}
//
if (isLeapMonth && !monthName.startsWith('闰')) {
monthName = '闰' + monthName
}
lunarDate.value = `农历${monthName}${lunarDay}`
} catch (error) {
console.error('农历计算错误:', error)
lunarDate.value = '农历计算中...'
}
}
//
const updateScale = () => {
if (!containerRef.value) return
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
//
const scaleX = windowWidth / BASE_WIDTH
const scaleY = windowHeight / BASE_HEIGHT
//
const scale = Math.min(scaleX, scaleY)
//
containerRef.value.style.transform = `scale(${scale})`
containerRef.value.style.transformOrigin = 'top left'
// 使
const scaledWidth = BASE_WIDTH * scale
const scaledHeight = BASE_HEIGHT * scale
const offsetX = (windowWidth - scaledWidth) / 2
const offsetY = (windowHeight - scaledHeight) / 2
containerRef.value.style.left = `${offsetX}px`
containerRef.value.style.top = `${offsetY}px`
}
let timer = null
onMounted(() => {
updateDateTime()
timer = setInterval(updateDateTime, 60000) //
// 86
document.body.style.overflow = 'hidden'
document.documentElement.style.overflow = 'hidden'
//
updateScale()
//
window.addEventListener('resize', updateScale)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
window.removeEventListener('resize', updateScale)
// 86
document.body.style.overflow = 'auto'
document.documentElement.style.overflow = 'auto'
})
</script>
<style scoped>
.tv86-viewport {
width: 100vw;
height: 100vh;
background: #000000;
overflow: hidden;
position: relative;
}
.tv86-container {
width: 3840px;
height: 2160px;
background:
radial-gradient(ellipse at 25% 25%, rgba(64, 158, 255, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 75% 75%, rgba(139, 92, 246, 0.06) 0%, transparent 50%),
linear-gradient(135deg, #0a0e1a 0%, #0f1419 25%, #131825 50%, #0f1419 75%, #0a0e1a 100%);
display: flex;
flex-direction: column;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
color: #ffffff;
overflow: hidden;
padding: 40px;
box-sizing: border-box;
position: absolute;
}
/* 顶部时间日期天气区域 */
.header-datetime {
height: 200px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40px;
}
.datetime-info {
display: flex;
align-items: center;
gap: 80px;
font-size: 56px;
font-weight: 600;
}
.date {
color: #ffffff;
font-size: 56px;
font-weight: 600;
}
.weekday {
color: #409EFF;
font-size: 56px;
font-weight: 600;
}
.lunar {
color: #67C23A;
font-size: 56px;
font-weight: 600;
}
.weather {
color: #E6A23C;
font-size: 56px;
font-weight: 600;
}
.temperature {
color: #F56C6C;
font-size: 56px;
font-weight: 600;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
gap: 60px;
min-height: 0;
}
/* 左侧区域 */
.left-section {
flex: 2;
display: flex;
flex-direction: column;
gap: 40px;
}
.left-top {
/* height: 400px; */
flex-shrink: 0;
}
.left-bottom {
flex: 1;
min-height: 0;
}
/* 右侧区域 */
.right-section {
flex: 1;
}
/* 确保内容始终适配,移除响应式媒体查询 */
/* 页面会通过JavaScript自动缩放来适配不同屏幕尺寸 */
</style>
Loading…
Cancel
Save