Files
datav---Cattle-Industry/src/components/Home.vue
2025-12-12 11:08:17 +08:00

2192 lines
64 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dashboard-container">
<!-- 左侧面板 -->
<aside class="dashboard-left">
<!-- 全国牛单价排行榜 -->
<div class="panel price-ranking-panel">
<div class="panel-header">
<h3>全国牛单价排行榜/</h3>
</div>
<div class="price-table">
<div class="price-table-header">
<div>序号</div>
<div>省份</div>
<div>地区</div>
<div>品种</div>
<div>单价</div>
<div>时间</div>
</div>
<div v-for="(row, idx) in nationalPriceTableSortedRows" :key="row.id" class="price-table-row">
<div>{{ idx + 1 }}</div>
<div>{{ row.province }}</div>
<div>{{ row.location }}</div>
<div>{{ row.breed }}</div>
<div class="price-value after-bar">{{ formatPrice(row.price) }}</div>
<div>{{ formatDate(row.time) }}</div>
</div>
</div>
</div>
<!-- 全国牛存栏量模块 -->
<div class="panel slaughter-panel">
<div class="panel-header">
<h3>全国牛存栏量</h3>
</div>
<div class="total-stock">
<div class="total-number">
<!-- <span class="currency"></span> -->
<!-- <span class="value">{{ nationalLivestockDisplay }}</span> -->
<!-- <span class="unit"></span> -->
</div>
<div class="echarts-container">
<v-chart
ref="nationalLivestockYearChart"
class="national-livestock-year-chart"
:option="nationalLivestockYearOption"
autoresize
/>
</div>
<!-- <div class="data-notes">
<div class="data-source">数据来源国家统计/行业估算示例该值为全国牛总存栏量基准</div>
</div> -->
</div>
</div>
<!-- 已移除不同品种年度总销售额柱状图模块 -->
</aside>
<!-- 中间地图区域 -->
<section class="dashboard-center">
<!-- 自定义无数据提示框 -->
<div v-if="showNoDataToast" class="no-data-toast">
{{ noDataMessage }}
</div>
<!-- 地图容器 -->
<div class="map-container">
<Map3D @province-click="handleProvinceClick" />
<!-- 跳转加载动画覆盖层 -->
<div v-if="isJumpLoading" class="jump-loading-overlay">
<div class="jump-spinner"></div>
<div class="jump-loading-text">正在请求 {{ loadingProvince }} 数据...</div>
</div>
</div>
</section>
<!-- 右侧面板 -->
<aside class="dashboard-right">
<!-- 品种单价排行榜替换原地区-品种明细 -->
<div class="panel price-ranking-panel">
<div class="panel-header">
<h3>品种单价排行榜/</h3>
<select v-model="rightSelectedBreed" class="breed-selector">
<option v-if="breedsLoading" disabled>加载中...</option>
<option v-else-if="rightBreedOptions.length === 0" disabled>暂无数据</option>
<option v-for="b in rightBreedOptions" :key="b" :value="b">{{ b }}</option>
</select>
</div>
<div class="species-price-table">
<div class="species-price-table-header">
<div>品种</div>
<div>省份</div>
<div>地区</div>
<div>单价(/)</div>
</div>
<div v-for="(row, idx) in speciesPriceTableSortedRows" :key="row.id" class="species-price-table-row">
<div>{{ row.breed }}</div>
<div>{{ row.province }}</div>
<div>{{ row.region }}</div>
<div>{{ formatPrice(row.price) }}</div>
</div>
</div>
</div>
<!-- 全国省份单价排行榜/ -->
<div class="panel price-ranking-panel">
<div class="panel-header">
<h3>全国省份平均单价排行榜/</h3>
</div>
<div class="echarts-container">
<v-chart
ref="provincePriceRankingChart"
class="province-price-ranking-chart"
:option="nationalProvincePriceRankingOption"
autoresize
/>
</div>
</div>
<!-- 全国牛出栏率模块年度全国出栏总量 -->
<div class="panel livestock-panel">
<div class="panel-header">
<h3>全国牛出栏量</h3>
</div>
<div class="echarts-container">
<v-chart
ref="nationalSlaughterYearChart"
class="national-slaughter-year-chart"
:option="nationalSlaughterYearOption"
autoresize
/>
</div>
<!-- <div class="data-notes">
<div class="data-source">全国出栏总量单位万头数据来源国家统计/行业估算示例</div>
</div> -->
</div>
</aside>
</div>
</template>
<style scoped>
/* 自定义无数据提示框 */
.no-data-toast {
position: absolute;
top: 15%;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 20, 40, 0.9);
border: 1px solid #00ffff;
box-shadow: 0 0 15px rgba(0, 255, 255, 0.4);
color: #00ffff;
padding: 12px 24px;
border-radius: 4px;
z-index: 9999;
font-size: 16px;
font-weight: bold;
pointer-events: none;
animation: fadeInOut 0.3s ease-in-out;
}
@keyframes fadeInOut {
from { opacity: 0; transform: translate(-50%, -10px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
.dashboard-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding: 10px;
box-sizing: border-box;
position: relative; /* 允许作为背景层容器 */
min-height: 100vh; /* 充满视口高度,保证背景覆盖 */
background-color: #011819; /* 底层纯色背景,位于背景图之上、内容之下 */
}
/* 全国牛存栏量显示样式 */
.total-stock {
padding: 10px 5px;
}
.total-number {
display: flex;
align-items: baseline;
gap: 6px;
}
.total-number .currency {
color: #00ffff;
font-size: 22px;
font-weight: 700;
}
.total-number .value {
color: #00ffcc;
font-size: 36px;
font-weight: 800;
}
.total-number .unit {
color: #cfefff;
font-size: 16px;
font-weight: 600;
}
.national-livestock-year-chart {
width: 100%;
height: 190px;
}
.national-slaughter-year-chart {
width: 100%;
height: 150px;
margin-top: 10px;
}
.national-price-ranking-chart {
width: 100%;
height: 180px;
}
.province-price-ranking-chart {
width: 100%;
height: 100%;
min-height: 200px;
}
.price-ranking-panel {
display: flex;
flex-direction: column;
flex: 1;
}
.price-ranking-panel .echarts-container {
height: auto;
padding: 0 0 5px 0; /* 左侧去掉内边距,整体更靠左 */
flex: 1;
}
/* 品种单价排行榜列表样式(右侧) */
.species-price-table {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
height: 250px; /* 设置固定高度 */
overflow-y: auto;
position: relative;
}
.species-price-table-header,
.species-price-table-row {
display: grid;
grid-template-columns: 1.2fr 1fr 1fr 1.2fr; /* 品种/省份/地区/单价 */
gap: 12px;
align-items: center;
font-size: 14px;
}
.species-price-table-header {
color: #84acf0;
border-bottom: 1px solid rgba(132, 172, 240, 0.2);
padding: 10px 0;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
background: #0C2435;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
}
.species-price-table-row {
color: #eaf7ff;
}
/* 全国牛单价排行榜表格样式 */
.price-table {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
/* 表格自适应增高,撑满左侧面板剩余空间 */
flex: 1;
min-height: 0;
overflow-y: auto;
position: relative;
}
.price-table-header,
.price-table-row {
display: grid;
grid-template-columns: 0.6fr 1fr 1.2fr 1.2fr 1.4fr 0.9fr;
gap: 14px;
align-items: center;
font-size: 15px;
}
.price-table-header > div:nth-child(5),
.price-table-row > div:nth-child(5) {
padding-left: 12px;
}
.price-table-header > div:nth-child(4),
.price-table-row > div:nth-child(4) {
padding-right: 8px;
}
.price-table-header {
color: #84acf0;
border-bottom: 1px solid rgba(132, 172, 240, 0.2);
padding: 8px 0;
font-weight: bold;
position: sticky;
top: 0;
z-index: 10;
background: #0C2435;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
}
.price-table-row {
color: #eaf7ff;
}
/* 奇偶行颜色区分 */
/* .price-table-row:nth-child(odd) {
background: rgba(255, 255, 255, 0.03);
}
.price-table-row:nth-child(even) {
background: rgba(0, 212, 255, 0.06);
} */
/* 单价列条形图样式 */
.price-bar-cell {
display: flex;
align-items: center;
gap: 8px;
}
.price-bar-track {
flex: 1;
height: 12px; /* 条形更细 */
border-radius: 0;
background: transparent; /* 去掉背景色,仅保留条形图颜色 */
overflow: hidden;
}
.price-bar {
height: 100%;
border-radius: 0 0 0 0; /* 去掉左侧圆角,仅保留右侧 */
background: #4e73df; /* 与图二一致的蓝色 */
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.price-value {
min-width: 86px;
text-align: left;
color: #eaf7ff;
font-variant-numeric: tabular-nums;
}
.price-value.after-bar {
min-width: 86px;
text-align: left;
color: #eaf7ff;
font-weight: 600;
}
.dashboard-center {
flex: 1; /* 中心区域保持基准宽度 */
min-width: 300px;
display: flex;
flex-direction: column;
height: 100vh; /* 确保中间区域占满整个视口高度 */
padding: 10px;
}
.dashboard-left,
.dashboard-right {
flex: 0.6; /* 两侧数据栏进一步缩小至约60%相对权重 */
min-width: 300px; /* 保持最小宽度,避免内容截断 */
max-width: 520px;
}
/* 左侧数据栏按列布局并占满视口高度 */
.dashboard-left,
.dashboard-right {
display: flex;
flex-direction: column;
height: 100vh;
}
/* 中低分辨率下进一步缩小到70%,保证协调性 */
@media (max-width: 1366px) {
.dashboard-left,
.dashboard-right {
flex: 0.55;
}
}
@media (max-width: 768px) {
.dashboard-left,
.dashboard-center,
.dashboard-right {
flex: 100%;
}
}
/* 侧边数据栏面板样式,参考预警监测模块 */
.panel {
background: rgba(7, 59, 68, 0.15);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
margin-bottom: 15px;
}
.panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: #00d4ff;
opacity: 0.6;
}
.panel-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.price-ranking-panel .panel-header {
margin-bottom: 0;
}
.price-ranking-panel .price-table {
padding-top: 0;
}
.price-ranking-panel .species-price-table {
padding-top: 0;
}
.panel-header h3 {
color: #ffffff;
font-size: 16px;
font-weight: 700;
margin: 0;
}
.diamond-icon {
width: 12px;
height: 12px;
background: #00d4ff;
transform: rotate(45deg);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
/* 统一下拉选择样式 */
.region-select,
.breed-selector {
margin-left: auto;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(0, 212, 255, 0.3);
color: #ffffff;
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
}
.region-select option,
.breed-selector option {
background: #0c1426;
color: #ffffff;
}
</style>
<script>
import { ref } from 'vue'
import axios from 'axios'
import Map3D from './Map3D.vue'
import { use } from 'echarts/core'
import { CanvasRenderer, SVGRenderer } from 'echarts/renderers'
import { PieChart, BarChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
} from 'echarts/components'
import * as echarts from 'echarts'
import VChart from 'vue-echarts'
use([
CanvasRenderer,
SVGRenderer,
PieChart,
BarChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent
])
export default {
name: 'Home',
components: {
Map3D,
VChart
},
props: {
selectedProvince: {
type: String,
default: ''
}
},
emits: ['navigate-to-warning'],
setup(props, { emit }) {
const showNoDataToast = ref(false)
const noDataMessage = ref('')
let toastTimer = null
const isJumpLoading = ref(false)
const loadingProvince = ref('')
// 省份点击请求控制器(用于防抖与取消上一次未完成的请求)
let provinceClickController = null
// provinces 接口参数映射:仅处理指定示例,其余保持不变
const toApiProvinceParam = (name) => {
const map = {
'内蒙古自治区': '内蒙古',
'四川省': '四川',
'新疆维吾尔自治区': '新疆',
'西藏自治区': '西藏',
'宁夏回族自治区': '宁夏',
'广西壮族自治区': '广西',
'河北省': '河北',
'山东省': '山东',
'黑龙江省': '黑龙江',
'吉林省': '吉林',
'云南省': '云南',
'甘肃省': '甘肃',
'青海省': '青海',
'贵州省': '贵州',
'安徽省': '安徽',
}
return map[name] ?? name
}
// 处理省份点击事件
const handleProvinceClick = async (provinceName) => {
// 若正在跳转/请求中,直接忽略后续点击,避免重复触发
if (isJumpLoading.value) return
// 跳转前加载动画
loadingProvince.value = provinceName
isJumpLoading.value = true
// 取消上一次未完成的请求(防抖与竞态控制)
if (provinceClickController) {
try { provinceClickController.abort() } catch {}
}
provinceClickController = new AbortController()
// 外部接口调用超时控制从2秒提高到8秒
let url = ''
const timeoutId = setTimeout(() => {
try { provinceClickController.abort() } catch {}
}, 8000)
try {
const apiParam = toApiProvinceParam(provinceName)
url = `/api/cattle-data/provinces?province=${encodeURIComponent(apiParam)}`
const dailyUrl = `/api/cattle-data/province-daily-prices?province=${encodeURIComponent(apiParam)}`
const dailyReq = fetch(dailyUrl, { signal: provinceClickController.signal }).then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json()
}).catch(e => e)
const res = await fetch(url, { signal: provinceClickController.signal })
clearTimeout(timeoutId)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const raw = await res.json()
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
if (list && list.length > 0) {
// 接口成功且有数据,自动跳转到价格行情页
isJumpLoading.value = false
emit('navigate-to-warning', provinceName)
} else {
throw new Error('empty payload')
}
} catch (error) {
clearTimeout(timeoutId)
// 区分超时与其他错误
if (error?.name === 'AbortError') {
console.warn('[外部接口] 超时已中止请求 (8s):', url)
noDataMessage.value = '请求超时'
} else {
console.error('[外部接口] 调用失败:', error)
noDataMessage.value = '该地区暂无数据'
}
isJumpLoading.value = false
showNoDataToast.value = true
if (toastTimer) clearTimeout(toastTimer)
toastTimer = setTimeout(() => {
showNoDataToast.value = false
}, 2000)
} finally {
provinceClickController = null
}
}
return {
handleProvinceClick,
showNoDataToast,
noDataMessage,
isJumpLoading,
loadingProvince
}
},
data() {
return {
// 右侧品种单价排行榜下拉选项与选中值
rightBreedOptions: [],
rightSelectedBreed: '',
breedsLoading: false,
speciesPriceRows: [], // 品种单价排行(接口)
// 存栏率视图模式percent 或 count
livestockViewMode: 'percent',
// 全国牛总存栏量基准值
livestockBaseline: 100000000,
// 不同品种默认数据(单位:头)
livestockSpeciesData: [
{ name: '黄牛及改良牛', key: 'yellow', count: 87500000, color: '#00CDCD' },
{ name: '奶牛', key: 'dairy', count: 11000000, color: '#869DB0' },
{ name: '水牛', key: 'buffalo', count: 4750000, color: '#267EEF' },
{ name: '牦牛', key: 'yak', count: 16000000, color: '#42A2E1', globalShare: '>90%', domesticShare: '16%' }
],
// 出栏率统计校验信息
validationMessageSlaughter: '',
// 存栏率选中的地区
selectedRegionStock: '全国',
// 选中的品种
selectedBreed: '',
// 防疫统计柱状图配置
epidemicChartOption: {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: {
color: '#ffffff'
},
formatter: '{b}: {c}万头'
},
legend: {
data: ['口蹄疫防疫', '猪瘟防疫', '其他类型防疫'],
right: '10%',
top: '10%',
orient: 'vertical',
textStyle: {
color: '#ffffff',
fontSize: 12
},
itemWidth: 12,
itemHeight: 8
},
grid: {
left: '15%',
right: '35%',
bottom: '15%',
top: '15%'
},
xAxis: {
type: 'category',
data: ['口蹄疫防疫', '猪瘟防疫', '其他类型防疫'],
axisLine: {
lineStyle: {
color: '#00ffff'
}
},
axisLabel: {
color: '#ffffff',
fontSize: 10,
rotate: 0,
margin: 10
},
axisTick: {
alignWithLabel: true,
show: true
},
boundaryGap: true
},
yAxis: {
type: 'value',
name: '头数(万)',
nameTextStyle: {
color: '#00ffff',
fontSize: 12
},
axisLine: {
lineStyle: {
color: '#00ffff'
}
},
axisLabel: {
color: '#ffffff',
fontSize: 10
},
splitLine: {
lineStyle: {
color: 'rgba(0, 255, 255, 0.2)'
}
}
},
series: [{
name: '防疫统计',
type: 'bar',
data: [
{ value: 32.67, itemStyle: { color: '#00ffff' } },
{ value: 20.3, itemStyle: { color: '#84acf0' } },
{ value: 1.91, itemStyle: { color: '#96ceb4' } }
],
barWidth: '30%',
itemStyle: {
color: function(params) {
const colors = ['#00ffff', '#84acf0', '#96ceb4'];
return colors[params.dataIndex];
}
}
}]
},
// 出栏率分类数据(单位:头)与颜色(统一主题)
slaughterSpeciesData: [
{ name: '杂交改良牛', key: 'hybrid', count: 43000000, color: '#00CDCD', percent: 85 },
{ name: '奶公犊', key: 'dairy_bull_calf', count: 5000000, color: '#869DB0', percent: 10 },
{ name: '地方品种黄牛', key: 'local_yellow', minCount: 1500000, maxCount: 2000000, color: '#267EEF', minPercent: 3, maxPercent: 4 },
{ name: '其他品种', key: 'others', minCount: 500000, maxCount: 800000, color: '#42A2E1', minPercent: 1, maxPercent: 1.5 }
],
slaughterPieOption: {},
slaughterBarOption: {},
// 不同品种牛存栏率图表(已改为全国出栏率,此处不再使用)
livestockPieOption: {},
livestockBarOption: {},
// 全国牛出栏总量年度柱状图(单位:万头)
nationalSlaughterYearOption: {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: function(params) {
const p = params[0]
return `${p.axisValue}<br/>${p.marker}${p.seriesName}: ${p.value}`
}
},
grid: { left: '12%', right: '8%', bottom: '12%', top: '18%' },
xAxis: {
type: 'category',
data: [],
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 10 }
},
yAxis: {
type: 'value',
name: '数量',
nameTextStyle: { color: '#00ffff', fontSize: 11 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 10 },
splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } }
},
series: [{
name: '全国牛出栏总量',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
data: [],
lineStyle: { color: '#00CDCD', width: 2 },
itemStyle: { color: '#00CDCD' },
areaStyle: { color: 'rgba(0,205,205,0.15)' },
label: { show: true, position: 'top', color: '#cfefff', fontSize: 10, formatter: '{c}' }
}]
},
// 全国牛存栏量年度柱状图(单位:万头)
nationalLivestockYearOption: {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: function(params) {
const p = params[0]
return `${p.axisValue}<br/>${p.marker}${p.seriesName}: ${p.value}`
}
},
grid: { left: '12%', right: '8%', bottom: '12%', top: '18%' },
xAxis: {
type: 'category',
data: [],
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 10 }
},
yAxis: {
type: 'value',
name: '数量',
nameTextStyle: { color: '#00ffff', fontSize: 11 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 10 },
splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } }
},
series: [{
name: '全国牛存栏总量',
type: 'bar',
data: [],
barWidth: '40%',
itemStyle: { color: '#00CDCD' },
label: { show: true, position: 'top', color: '#cfefff', fontSize: 10, formatter: '{c}' }
}]
},
// 已移除2025年主要品种牛单价趋势预测配置
// 全国牛单价排行榜表格数据
nationalPriceTableRows: [
{ id: 1, province: '河北省', location: '石家庄市', breed: '安格斯牛', price: 14200, time: '2025-12-03' },
{ id: 2, province: '山东省', location: '济南市', breed: '荷斯坦牛', price: 17000, time: '2025-12-03' },
{ id: 3, province: '江苏省', location: '南京市', breed: '复洲黄牛', price: 14500, time: '2025-12-03' },
{ id: 4, province: '浙江省', location: '杭州市', breed: '西门塔尔牛', price: 16800, time: '2025-12-03' },
{ id: 5, province: '新疆维吾尔自治区', location: '乌鲁木齐市', breed: '夏洛莱牛', price: 16500, time: '2025-12-03' },
{ id: 6, province: '甘肃省', location: '兰州市', breed: '水牛', price: 17387, time: '2025-12-03' },
{ id: 7, province: '广东省', location: '广州市', breed: '安格斯牛', price: 14200, time: '2025-12-03' },
{ id: 8, province: '广西壮族自治区', location: '南宁市', breed: '荷斯坦牛', price: 17000, time: '2025-12-03' },
{ id: 9, province: '湖南省', location: '长沙市', breed: '复洲黄牛', price: 14500, time: '2025-12-03' },
{ id: 10, province: '河南省', location: '郑州市', breed: '西门塔尔牛', price: 16800, time: '2025-12-03' }
],
// 全国省份平均单价数据源(专用于右侧省份排行图表)
nationalProvinceAverageRows: [],
// 全国牛单价排行榜配置(单位:元/头)
nationalPriceRankingOption: {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: function(params) {
const p = params[0]
return `${p.axisValue}<br/>${p.marker}${p.seriesName}: ${p.value} 元/头`
}
},
grid: { left: '18%', right: '12%', bottom: '15%', top: '20%' },
xAxis: {
type: 'value',
name: '单价(元/头)',
nameTextStyle: { color: '#00ffff', fontSize: 11 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 10 },
splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } }
},
yAxis: {
type: 'category',
data: ['荷斯坦牛','西门塔尔牛','夏洛莱牛','复洲黄牛','安格斯牛','水牛'],
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 11 }
},
series: [{
name: '全国主要品种单价',
type: 'bar',
data: [17000, 16800, 16500, 14500, 14200, 17387],
barWidth: '45%',
itemStyle: { color: '#00CDCD' },
label: { show: true, position: 'right', color: '#cfefff', fontSize: 10, formatter: '{c} 元/头' }
}]
},
// 地区-品种明细数据
detailRows: [
{ id: 1, region: '河北省', breed: '安格斯牛', stock: 68000, sold: 12000, price: 14200 },
{ id: 2, region: '山东省', breed: '荷斯坦牛', stock: 52000, sold: 9000, price: 17000 },
{ id: 3, region: '江苏省', breed: '复洲黄牛', stock: 72000, sold: 15000, price: 14500 },
{ id: 4, region: '浙江省', breed: '西门塔尔牛', stock: 61000, sold: 11000, price: 16800 },
{ id: 5, region: '新疆维吾尔自治区', breed: '夏洛莱牛', stock: 83000, sold: 13000, price: 16500 },
{ id: 6, region: '甘肃省', breed: '水牛', stock: 47000, sold: 8000, price: 17387 },
{ id: 7, region: '广东省', breed: '安格斯牛', stock: 55000, sold: 10000, price: 14200 },
{ id: 8, region: '广西壮族自治区', breed: '荷斯坦牛', stock: 43000, sold: 7500, price: 17000 },
{ id: 9, region: '湖南省', breed: '复洲黄牛', stock: 65000, sold: 12500, price: 14500 },
{ id: 10, region: '河南省', breed: '西门塔尔牛', stock: 58000, sold: 9800, price: 16800 }
],
// 耳标统计数据
earTagStats: {
completed: 45678,
planned: 52000
},
_provinceDailyTimer: null,
// 耳标佩戴统计堆叠柱状图配置
earTagChartOption: {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: {
color: '#ffffff'
},
formatter: function(params) {
let result = params[0].name + '<br/>';
params.forEach(function(item) {
result += item.marker + item.seriesName + ': ' + item.value + '头<br/>';
});
return result;
}
},
legend: {
data: ['已佩戴', '未佩戴'],
top: '5%',
textStyle: {
color: '#ffffff',
fontSize: 12
},
itemWidth: 12,
itemHeight: 8
},
grid: {
left: '15%',
right: '10%',
bottom: '15%',
top: '25%'
},
xAxis: {
type: 'value',
name: '数量(头)',
nameTextStyle: {
color: '#00ffff',
fontSize: 12
},
axisLine: {
lineStyle: {
color: '#00ffff'
}
},
axisLabel: {
color: '#ffffff',
fontSize: 10
},
splitLine: {
lineStyle: {
color: 'rgba(0, 255, 255, 0.2)'
}
}
},
yAxis: {
type: 'category',
data: ['东方养殖场', '西部牧场', '南山农场', '北岭牧业', '中心养殖基地'],
axisLine: {
lineStyle: {
color: '#00ffff'
}
},
axisLabel: {
color: '#ffffff',
fontSize: 10
}
},
series: [
{
name: '已佩戴',
type: 'bar',
stack: '总量',
data: [8500, 12000, 9800, 7200, 8178],
itemStyle: {
color: '#00ffff'
}
},
{
name: '未佩戴',
type: 'bar',
stack: '总量',
data: [1500, 2200, 1800, 1300, 1322],
itemStyle: {
color: '#84acf0'
}
}
]
},
// 牛只参保统计圆形进度图配置
insuranceChartOption: {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}头 ({d}%)',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: {
color: '#ffffff'
}
},
series: [
{
name: '参保统计',
type: 'pie',
radius: ['70%', '90%'],
center: ['50%', '50%'],
startAngle: 90,
avoidLabelOverlap: false,
label: {
show: false
},
labelLine: {
show: false
},
data: [
{
value: 160000,
name: '已参保',
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 1,
colorStops: [{
offset: 0, color: '#00ffff'
}, {
offset: 1, color: '#0080ff'
}]
}
}
},
{
value: 40000,
name: '未参保',
itemStyle: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
]
}
]
},
// 已移除:不同品种年度总销售额柱状图配置
}
},
computed: {
nationalLivestockCount() {
return this.livestockBaseline || 0
},
nationalLivestockDisplay() {
try {
return (this.nationalLivestockCount || 0).toLocaleString('zh-CN')
} catch (e) {
return String(this.nationalLivestockCount || 0)
}
},
totalLivestock() {
// 以数据源计算总存栏量(万头)
return this.livestockSpeciesData.reduce((sum, s) => sum + Math.round(s.count / 10000), 0)
},
filteredDetailRows() {
if (!this.selectedBreed) {
return this.detailRows
}
return this.detailRows.filter(row => row.breed === this.selectedBreed)
},
// 品种单价排行榜(右侧)基于地区-品种明细,按价格升序
speciesPriceTableSortedRows() {
let rows = (Array.isArray(this.speciesPriceRows) && this.speciesPriceRows.length > 0)
? this.speciesPriceRows
: (Array.isArray(this.detailRows) ? this.detailRows.map(r => ({
id: r.id,
breed: r.breed,
region: r.region,
price: r.price
})) : [])
// 下拉选择生效:按包含关系过滤(处理“安格斯”匹配“安格斯牛”等)
if (this.rightSelectedBreed) {
rows = rows.filter(x => String(x.breed || '').includes(this.rightSelectedBreed))
}
rows.sort((a, b) => a.price - b.price)
return rows
},
// 全国省份平均单价排行榜(右侧图表):优先使用 nationalProvinceAverageRows否则回退 nationalPriceTableRows
nationalProvincePriceRankingOption() {
const base = (Array.isArray(this.nationalProvinceAverageRows) && this.nationalProvinceAverageRows.length > 0)
? [...this.nationalProvinceAverageRows]
: (Array.isArray(this.nationalPriceTableRows) ? [...this.nationalPriceTableRows] : [])
base.sort((a, b) => a.price - b.price)
const xCategories = base.map(r => r.province)
const prices = base.map(r => r.price)
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: function(params) {
const p = params[0]
return `${p.axisValue}<br/>${p.seriesName}: ${p.value} 元/斤`
}
},
grid: { left: '2%', right: '2%', bottom: '2%', top: '18%', containLabel: true },
xAxis: {
type: 'category',
data: xCategories,
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 9, interval: 0 },
},
yAxis: {
type: 'value',
name: '单价(元/斤)',
nameTextStyle: { color: '#00ffff', fontSize: 11 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 10 },
min: 5,
max: 25,
interval: 5,
splitLine: { lineStyle: { color: 'rgba(0,255,255,0.2)' } }
},
series: [{
name: '省份单价',
type: 'bar',
data: prices,
barWidth: '42%',
barCategoryGap: '28%',
itemStyle: { color: '#00E1E1' },
label: { show: true, position: 'top', color: '#cfefff', fontSize: 10, formatter: '{c} ' }
}],
dataZoom: [
{
type: 'inside',
xAxisIndex: 0,
startValue: 0,
endValue: xCategories.length - 1,
zoomLock: true,
filterMode: 'empty'
}
]
}
},
// 单价条形图需要的最大值(用于计算宽度百分比)
priceBarMax() {
const arr = Array.isArray(this.nationalPriceTableRows) ? this.nationalPriceTableRows.map(r => r.price) : []
const max = Math.max(...arr, 1)
return max
},
// 单价升序排列后的行数据
nationalPriceTableSortedRows() {
const rows = Array.isArray(this.nationalPriceTableRows) ? [...this.nationalPriceTableRows] : []
rows.sort((a, b) => a.price - b.price)
return rows
}
},
watch: {
rightSelectedBreed: {
handler(breed) {
if (breed) {
this.fetchSpeciesPriceRanking(breed)
}
},
immediate: true
}
},
mounted() {
// 页面挂载后拉取品种列表以填充右侧下拉框
console.log('[Home] mounted: fetchBreeds 即将调用')
this.fetchBreeds()
},
created() {
// 保险起见,在 created 阶段也触发一次,避免某些场景 mounted 未执行
console.log('[Home] created: fetchBreeds 即将调用')
this.fetchBreeds()
},
methods: {
// 格式化价格显示(千分位)
formatPrice(value) {
try {
return Number(value || 0).toLocaleString('zh-CN')
} catch (e) {
return String(value || 0)
}
},
formatDate(value) {
if (!value) {
const t = new Date()
const m = String(t.getMonth() + 1).padStart(2, '0')
const day = String(t.getDate()).padStart(2, '0')
return `${m}-${day}`
}
const d = new Date(value)
if (Number.isNaN(d.getTime())) return String(value)
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${m}-${day}`
},
// 计算条形宽度样式
getPriceBarStyle(price) {
const max = this.priceBarMax || 1
const ratio = Math.max(0, Math.min(1, Number(price || 0) / max))
// 增强对比非线性拉伸最小5%最大100%,采用幂函数放大差异
const percent = 5 + Math.pow(ratio, 0.6) * 95
return { width: `${percent.toFixed(2)}%` }
},
// 拉取全国牛单价排行榜数据并填充表格/图表字段type/省份province/单价price
async fetchNationalPriceRanking() {
const url = '/api/cattle-data'
try {
const res = await fetch(url)
const raw = await res.json()
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
const rows = list.map((item, idx) => ({
id: item.id ?? idx + 1,
province: item.province ?? item.provinceName ?? '',
location: item.location ?? '',
breed: item.type ?? item.breed ?? '',
price: Number(item.price),
time: item.time ?? item.priceDate ?? item.date ?? ''
})).filter(r => r.province && r.breed && Number.isFinite(r.price))
// 更新数据源(驱动左侧表格与右侧省份排行图表)
this.nationalPriceTableRows = rows
} catch (e) {
console.warn('获取全国牛单价排行榜失败:', e)
}
},
// 拉取品种单价排行榜数据
async fetchSpeciesPriceRanking(breed) {
const url = `/api/cattle-data?type=${breed}`
try {
const res = await fetch(url)
const raw = await res.json()
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
const rows = list.map((item, idx) => ({
id: item.id ?? idx + 1,
province: item.province ?? '',
region: item.location ?? '',
breed: item.type ?? item.breed ?? '',
price: Number(item.price)
})).filter(r => r.province && r.region && r.breed && Number.isFinite(r.price))
this.speciesPriceRows = rows
} catch (e) {
console.warn(`获取品种 ${breed} 单价排行失败:`, e)
}
},
// 拉取全国省份平均单价排行榜数据字段province/provincePrice
async fetchNationalProvinceAverages() {
const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
const base = new Date()
let tryDate = fmt(base)
for (let i = 0; i < 7; i++) {
const url = `/api/cattle-data/province-daily-prices?priceDate=${encodeURIComponent(tryDate)}`
try {
const res = await fetch(url)
const raw = await res.json()
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
const rows = list.map((item, idx) => ({
id: item.id ?? idx + 1,
province: item.province ?? item.provinceName ?? '',
price: Number(item.price ?? item.provincePrice)
})).filter(r => r.province && Number.isFinite(r.price))
if (rows.length > 0) {
this.nationalProvinceAverageRows = rows
break
}
} catch (e) {}
const d = new Date(tryDate)
d.setDate(d.getDate() - 1)
tryDate = fmt(d)
}
},
// 拉取品种列表,使用 breedName 作为下拉项
async fetchBreeds() {
const url = '/api/cattle-data/breeds'
this.breedsLoading = true
try {
console.log('[Home] 调用接口:', url)
const res = await fetch(url)
const raw = await res.json()
console.log('[Home] breeds 接口返回原始数据:', raw)
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : [])
const names = list
.map(it => it?.breedName ?? it?.name ?? it?.breed)
.filter(Boolean)
const uniq = Array.from(new Set(names))
console.log('[Home] 解析后的品种列表:', uniq)
if (uniq.length > 0) {
const prev = this.rightSelectedBreed
this.rightBreedOptions = uniq
if (!prev || !uniq.includes(prev)) {
this.rightSelectedBreed = uniq[0]
}
}
} catch (e) {
console.warn('[Home] 获取品种列表失败:', e)
} finally {
this.breedsLoading = false
console.log('[Home] breedsLoading 结束,状态:', this.breedsLoading)
}
},
// 构建环形图(占比)
buildLivestockPieOption() {
const baseline = this.livestockBaseline
const species = this.livestockSpeciesData
const names = species.map(s => s.name)
const percents = species.map(s => Math.round(s.count / baseline * 100))
const pieData = percents.map((v, i) => ({
value: v,
name: names[i],
itemStyle: { color: species[i].color }
}))
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: (params) => `${params.name}<br/>占比:${params.value}%`
},
series: [{
name: '占比(%)',
type: 'pie',
radius: ['35%', '49%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
label: {
show: true,
position: 'outside',
fontSize: 11,
color: '#ffffff',
formatter: (p) => `${p.name}\n${p.value}%`
},
labelLine: { show: true, length: 12, length2: 8, lineStyle: { color: '#ffffff', width: 1 } },
emphasis: {
label: { show: true, fontSize: 12, fontWeight: 'bold', color: '#00ffff' }
},
data: pieData
}]
}
},
// 通过接口更新全国存栏/出栏年度数据以2023年为例
async fetchNationalInventorySlaughter() {
// 通过 Vite 代理避免浏览器 CORS使用相对路径
const url = '/api/cattle-data/national'
try {
const res = await fetch(url)
const raw = await res.json()
// 接口可能返回 { code, data: {...} } 或直接返回对象,统一取 payload
const payload = raw && typeof raw === 'object' && raw.data ? raw.data : raw
// 固定横轴为 2023/2024/2025纵轴填充数值单位万头缺失则为 null
const years = ['2023年', '2024年', '2025年']
const readNumber = (obj, key) => {
if (!obj) return null
const v = obj[key]
const n = typeof v === 'string' ? Number(v) : (typeof v === 'number' ? v : null)
return Number.isFinite(n) ? n : null
}
const inventoryValues = [
readNumber(payload, 'nationalInventory23th') ?? readNumber(payload, 'nationalInventory23'),
readNumber(payload, 'nationalInventory24th') ?? readNumber(payload, 'nationalInventory24'),
readNumber(payload, 'nationalInventory25th') ?? readNumber(payload, 'nationalInventory25')
]
const slaughterValues = [
readNumber(payload, 'nationalSlaughter23th') ?? readNumber(payload, 'nationalSlaughter23'),
readNumber(payload, 'nationalSlaughter24th') ?? readNumber(payload, 'nationalSlaughter24'),
readNumber(payload, 'nationalSlaughter25th') ?? readNumber(payload, 'nationalSlaughter25')
]
// 更新“全国牛存栏量”
this.nationalLivestockYearOption.xAxis.data = years
this.nationalLivestockYearOption.series[0].data = inventoryValues
this.nationalLivestockYearOption.yAxis.name = '数量(万头)'
this.nationalLivestockYearOption.series[0].label = { ...this.nationalLivestockYearOption.series[0].label, formatter: '{c} 万头' }
this.nationalLivestockYearOption.tooltip.formatter = function(params) {
const p = params[0]
return `${p.axisValue}<br/>${p.marker}${p.seriesName}: ${p.value} 万头`
}
this.nationalLivestockYearOption = { ...this.nationalLivestockYearOption }
// 更新“全国牛出栏量”
this.nationalSlaughterYearOption.xAxis.data = years
this.nationalSlaughterYearOption.series[0].data = slaughterValues
this.nationalSlaughterYearOption.yAxis.name = '数量(万头)'
this.nationalSlaughterYearOption.series[0].label = { ...this.nationalSlaughterYearOption.series[0].label, formatter: '{c} 万头' }
this.nationalSlaughterYearOption.tooltip.formatter = function(params) {
const p = params[0]
return `${p.axisValue}<br/>${p.marker}${p.seriesName}: ${p.value} 万头`
}
this.nationalSlaughterYearOption = { ...this.nationalSlaughterYearOption }
} catch (e) {
console.warn('获取全国存栏/出栏接口数据失败:', e)
}
},
// 更新“全国牛存栏量年度柱状图”指定年份的值(单位:万头)
updateLivestockYearValue(year, valueWan) {
const yearLabel = `${year}`
const x = this.nationalLivestockYearOption.xAxis.data || []
const idx = x.indexOf(yearLabel)
if (idx >= 0) {
const arr = this.nationalLivestockYearOption.series[0].data || []
arr[idx] = Number(valueWan)
// 触发响应式更新
this.nationalLivestockYearOption = { ...this.nationalLivestockYearOption }
}
},
// 更新“全国牛出栏量年度折线图”指定年份的值(单位:万头)
updateSlaughterYearValue(year, valueWan) {
const yearLabel = `${year}`
const x = this.nationalSlaughterYearOption.xAxis.data || []
const idx = x.indexOf(yearLabel)
if (idx >= 0) {
const arr = this.nationalSlaughterYearOption.series[0].data || []
arr[idx] = Number(valueWan)
// 触发响应式更新
this.nationalSlaughterYearOption = { ...this.nationalSlaughterYearOption }
}
},
// 构建柱状图(数量)
buildLivestockBarOption() {
const species = this.livestockSpeciesData
const names = species.map(s => s.name)
const countsWan = species.map(s => Math.round(s.count / 10000))
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: (params) => {
const p = params[0]
return `${p.name}<br/>存栏量:${p.value} 万头`
}
},
grid: { left: '12%', right: '6%', top: '15%', bottom: '12%', containLabel: true },
xAxis: {
type: 'category',
data: names,
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 11, rotate: 0 }
},
yAxis: {
type: 'value',
name: '数量(万头)',
nameTextStyle: { color: '#00ffff', fontSize: 12 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisLabel: { color: '#ffffff', fontSize: 11 },
splitLine: { lineStyle: { color: 'rgba(0, 255, 255, 0.2)' } }
},
series: [{
name: '数量(万头)',
type: 'bar',
barWidth: '50%',
label: { show: true, position: 'top', color: '#eaffff', fontSize: 11, formatter: '{c} 万头' },
data: countsWan.map((v, i) => ({ value: v, itemStyle: { color: species[i].color } }))
}]
}
},
// 刷新两个图表
refreshLivestockOption() {
this.livestockPieOption = this.buildLivestockPieOption()
this.livestockBarOption = this.buildLivestockBarOption()
},
filterByBreed() {
// 品种选择变化时的处理逻辑
// filteredDetailRows计算属性会自动更新
},
updateLivestockData() {
// 当前未提供地区维度的权威数据;保持全国基准展示
this.refreshLivestockOption()
},
// 出栏率——环形图配置
buildSlaughterPieOption() {
const seriesData = this.slaughterSpeciesData.map(s => {
if (s.percent != null) {
return { value: s.percent, name: s.name, itemStyle: { color: s.color }, labelRange: `${s.percent}%` }
}
const mid = ((s.minPercent + s.maxPercent) / 2).toFixed(2)
return { value: +mid, name: s.name, itemStyle: { color: s.color }, labelRange: `${s.minPercent}-${s.maxPercent}%` }
})
return {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: params => `${params.marker}${params.name}<br/>占比:${seriesData[params.dataIndex].labelRange}`
},
legend: { show: false },
series: [{
name: '出栏占比',
type: 'pie',
radius: ['35%', '49%'],
center: ['44%', '50%'],
avoidLabelOverlap: true,
label: {
show: true,
color: '#eaf7ff',
formatter: ({ dataIndex, name }) => `${name}\n${seriesData[dataIndex].labelRange}`,
fontSize: 12
},
labelLine: { show: true, length: 8, length2: 6 },
data: seriesData
}]
}
},
// 出栏率——柱状图配置(区间值展示为中位数数值,标签文本展示区间)
buildSlaughterBarOption() {
const data = this.slaughterSpeciesData.map(s => {
if (s.count != null) {
return { value: Math.round(s.count / 10000), labelText: `${Math.round(s.count / 10000)}万头`, color: s.color, name: s.name }
}
const midCount = Math.round(((s.minCount + s.maxCount) / 2) / 10000)
const labelText = `${Math.round(s.minCount / 10000)}-${Math.round(s.maxCount / 10000)}万头`
return { value: midCount, labelText, color: s.color, name: s.name }
})
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00ffff',
borderWidth: 1,
textStyle: { color: '#ffffff' },
formatter: params => {
const p = params[0]
const d = data[p.dataIndex]
return `${p.marker}${d.name}<br/>出栏量:${d.labelText}`
}
},
grid: { top: '20%', right: '6%', bottom: '15%', left: '12%', containLabel: true },
xAxis: {
type: 'category',
data: data.map(d => d.name),
axisLabel: { color: '#ffffff', fontSize: 12 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisTick: { lineStyle: { color: '#00ffff' } }
},
yAxis: {
type: 'value',
name: '出栏量(万头)',
nameTextStyle: { color: '#00ffff', fontSize: 12 },
axisLabel: { color: '#ffffff', fontSize: 11 },
axisLine: { lineStyle: { color: '#00ffff' } },
axisTick: { lineStyle: { color: '#00ffff' } },
splitLine: { lineStyle: { color: 'rgba(0, 255, 255, 0.2)' } }
},
series: [{
name: '出栏量',
type: 'bar',
data: data.map(d => ({ value: d.value, itemStyle: { color: d.color } })),
barWidth: '60%',
label: {
show: true,
position: 'top',
color: '#eaf7ff',
formatter: ({ dataIndex }) => data[dataIndex].labelText,
fontSize: 11
}
}]
}
},
refreshSlaughterOption() {
this.slaughterPieOption = this.buildSlaughterPieOption()
this.slaughterBarOption = this.buildSlaughterBarOption()
},
validateSlaughterData() {
const categories = ['hybrid', 'dairy_bull_calf', 'local_yellow', 'others']
const keysPresent = new Set(this.slaughterSpeciesData.map(s => s.key))
if (!categories.every(k => keysPresent.has(k))) {
this.validationMessageSlaughter = '数据完整性异常:缺少分类项,请检查数据源。'
return false
}
let sum = 0
this.slaughterSpeciesData.forEach(s => {
if (s.percent != null) sum += s.percent
else sum += (s.minPercent + s.maxPercent) / 2
})
const ok = Math.abs(sum - 100) <= 0.5
this.validationMessageSlaughter = ok ? '' : `占比合计为${sum.toFixed(2)}%未满足100%±0.5%校验。`
return ok
},
ingestSlaughterRecords(records) {
if (!Array.isArray(records)) return
const map = { hybrid: 0, dairy_bull_calf: 0, local_yellow: 0, others: 0 }
for (let i = 0; i < records.length; i++) {
const r = records[i]
if (map[r.categoryKey] != null) map[r.categoryKey] += r.count || 0
}
this.slaughterSpeciesData = this.slaughterSpeciesData.map(s => {
if (s.count != null) return { ...s, count: map[s.key] || s.count }
if (map[s.key] && map[s.key] > 0) {
const val = map[s.key]
return { ...s, minCount: Math.min(val, s.minCount ?? val), maxCount: Math.max(val, s.maxCount ?? val) }
}
return s
})
this.refreshSlaughterOption()
this.validateSlaughterData()
}
},
mounted() {
// 页面加载时分别初始化数据
this.refreshLivestockOption()
this.refreshSlaughterOption()
this.validateSlaughterData()
// 拉取全国存栏/出栏年度数据并更新图表
this.fetchNationalInventorySlaughter()
// 拉取全国牛单价排行榜数据
this.fetchNationalPriceRanking()
// 拉取全国省份平均单价排行榜数据
this.fetchNationalProvinceAverages()
if (this._provinceDailyTimer) clearTimeout(this._provinceDailyTimer)
const schedule = () => {
const now = new Date()
const next = new Date(now)
next.setHours(12, 0, 0, 0)
if (now.getTime() >= next.getTime()) next.setDate(next.getDate() + 1)
const delay = next.getTime() - now.getTime()
this._provinceDailyTimer = setTimeout(async () => {
await this.fetchNationalProvinceAverages()
schedule()
}, Math.max(1000, delay))
}
schedule()
},
beforeUnmount() {
if (this._provinceDailyTimer) {
clearTimeout(this._provinceDailyTimer)
this._provinceDailyTimer = null
}
}
}
</script>
<style scoped>
.dashboard-container {
/* 移除flex布局使用App.vue中的fixed布局 */
background: transparent;
color: #ffffff;
font-family: 'Microsoft YaHei', sans-serif;
}
/* 地图容器样式 */
.map-container {
position: relative;
width: 100%;
height: 100%; /* 从68%改为100%,让地图占满整个中间区域 */
margin-top: 0; /* 移除负边距 */
}
/* 跳转加载动画样式 */
.jump-loading-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
pointer-events: none;
}
.jump-spinner {
width: 36px;
height: 36px;
border: 3px solid rgba(0,212,255,0.35);
border-top-color: #00d4ff;
border-radius: 50%;
animation: jumpSpin 1s linear infinite;
margin-bottom: 10px;
}
.jump-loading-text {
color: #eaf7ff;
font-size: 14px;
}
@keyframes jumpSpin {
to { transform: rotate(360deg); }
}
.lowest-price-panel h3 {
color: #00d4ff;
font-size: 16px;
margin: 0 0 15px 0;
text-align: left;
font-weight: bold;
}
.price-item {
background: rgba(0, 255, 255, 0.1);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 6px;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
min-height: 80px; /* 设置最小高度确保内容显示 */
}
.price-item:hover {
background: rgba(0, 255, 255, 0.2);
border-color: #00d4ff;
transform: translateY(-2px);
}
.breed-name {
color: #00d4ff;
font-size: 13px; /* 增加字体大小 */
font-weight: bold;
margin-bottom: 6px; /* 增加底部间距 */
text-align: center;
}
.province-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px; /* 增加间距 */
}
.province {
color: #ffffff;
font-size: 12px; /* 增加字体大小 */
}
.price {
color: #00ff88;
font-size: 11px; /* 增加字体大小 */
font-weight: bold;
}
/* 左右侧面板容器边框 */
.dashboard-left::before,
.dashboard-right::before {
content: '';
position: absolute;
top: 0px;
left: 10px;
right: 10px;
border: 2px solid #00d4ff;
border-radius: 8px;
pointer-events: none;
box-shadow:
0 0 10px rgba(0, 212, 255, 0.3),
inset 0 0 10px rgba(0, 212, 255, 0.1);
}
.dashboard-left::after,
.dashboard-right::after {
content: '';
position: absolute;
top: 10px;
left: 10px;
right: 10px;
/* bottom: 10px; */
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 6px;
pointer-events: none;
}
/* 合并模块样式 */
.combined-panel {
height: 70vh; /* 进一步增加到70%视口高度 */
}
.combined-content {
display: flex;
gap: 15px;
height: calc(100% - 60px); /* 减去标题高度 */
}
/* 上下布局样式 */
.combined-content.vertical-layout {
flex-direction: column;
}
.chart-section {
flex: 1;
display: flex;
flex-direction: column;
background: rgba(0, 20, 40, 0.3);
border-radius: 4px;
}
/* 上下布局时的图表区域样式 */
.vertical-layout .chart-section {
min-height: 220px; /* 增加最小高度,提供更多空间 */
}
.chart-section h4 {
color: #00d4ff;
font-size: 14px;
margin: 0 0 10px 0;
text-align: left;
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
}
.sales-section .chart,
.livestock-section .livestock-chart {
flex: 1;
min-height: 200px; /* 减小最小高度 */
}
.echarts-container {
flex: 1;
display: flex;
flex-direction: column;
}
/* 面板样式 */
.panel {
background: rgba(7, 59, 68, 0.15);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 6px;
margin-bottom: 5px;
backdrop-filter: blur(5px);
position: relative;
overflow: hidden;
box-shadow:
0 0 8px rgba(0, 212, 255, 0.2),
inset 0 0 8px rgba(0, 212, 255, 0.05);
}
.panel-header {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
margin-bottom: 10px;
}
.panel-header::before {
content: '';
position: absolute;
bottom: 0;
left: 15px;
width: 30px;
height: 2px;
background: #00d4ff;
box-shadow: 0 0 4px rgba(0, 212, 255, 0.6);
}
.panel-header h3 {
color: #ffffff;
font-size: 16px;
padding-bottom: 5px;
font-weight: bold;
position: relative;
z-index: 1;
}
.panel-header h3::before {
content: '';
position: absolute;
left: -10px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 4px;
background: #00d4ff;
border-radius: 50%;
box-shadow: 0 0 6px rgba(0, 212, 255, 0.8);
}
.region-select:hover,
.breed-selector:hover {
border-color: #00ffff;
box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
}
.region-select option,
.breed-selector option {
background: rgba(0, 20, 40, 0.95);
color: #ffffff;
padding: 4px;
}
/* 存栏总数统计样式 */
.livestock-panel .echarts-container {
display: flex;
flex-direction: row;
gap: 10px; /* 两图间距约16px */
height:190px;
padding: 0 5px 5px 5px;
}
.total-display {
text-align: center;
/* margin-bottom: 1px; */
}
.total-number {
font-size: 24px;
font-weight: bold;
color: #00ffff;
}
.total-label {
font-size: 12px;
color: #cccccc;
margin-top: 5px;
}
.livestock-chart {
flex: 1;
width: 100%;
height: 350px;
}
/* 新增:存栏率的两个图表容器 */
.livestock-pie {
flex: 0 0 40%;
height: 100%;
}
.livestock-bar {
flex: 1;
height: 100%;
}
/* 统一图例样式,置于两图下方 */
.livestock-legend {
display: flex;
flex-wrap: wrap;
gap: 10px 10px;
align-items: center;
}
.livestock-legend .legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.livestock-legend .legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
box-shadow: 0 0 6px rgba(0, 212, 255, 0.5);
}
.livestock-legend .legend-label {
color: #eaf7ff;
font-size: 12px;
}
/* 小屏幕下纵向排列 */
@media (max-width: 768px) {
.livestock-panel .echarts-container {
flex-direction: column;
height: auto;
}
.livestock-pie,
.livestock-bar {
width: 100%;
height: 280px;
}
}
/* 出栏率统计样式 */
.slaughter-panel {
display: flex;
flex-direction: column;
/* 与“全国牛存栏率”模块保持一致的自适应面板高度 */
flex: 0 0 auto;
height: auto;
}
.slaughter-panel .echarts-container {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 16px;
/* 图表容器高度与“全国牛存栏率”模块一致 */
flex: 0 0 230px;
height: 230px;
}
.slaughter-pie {
flex: 0 0 42%;
height: 100%;
}
.slaughter-bar {
flex: 1;
height: 100%;
}
.slaughter-legend {
display: flex;
flex-wrap: wrap;
gap: 10px 10px;
align-items: center;
margin-top: 8px;
}
.slaughter-legend .legend-item { display: flex; align-items: center; gap: 6px; }
.slaughter-legend .legend-color { width: 12px; height: 12px; border-radius: 2px; box-shadow: 0 0 6px rgba(0, 212, 255, 0.5); }
.slaughter-legend .legend-label { color: #eaf7ff; font-size: 12px; }
.validation-message { margin-top: 6px; color: #ffcf6e; font-size: 12px; }
@media (max-width: 768px) {
.slaughter-panel { height: auto; }
.slaughter-panel .echarts-container { flex-direction: column; height: auto; }
.slaughter-pie, .slaughter-bar { width: 100%; height: 280px; }
}
.flip-card:hover .flip-card-inner {
transform: rotateY(180deg);
}
.flip-card-front {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
background: rgba(0, 255, 255, 0.15);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
backdrop-filter: blur(10px);
}
.card-number {
font-size: 24px;
font-weight: bold;
color: #00ffff;
margin-bottom: 8px;
}
.card-label {
font-size: 12px;
color: #cccccc;
text-align: center;
line-height: 1.2;
}
.chart-container {
flex: 1;
min-height: 200px;
}
.ear-tag-chart {
width: 100%;
height:93%;
}
/* 已移除:品种单价排行榜样式 */
/* 明细表样式 */
.detail-table {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
padding: 15px;
height: 520px;
overflow-y: auto;
}
.detail-header, .detail-row {
display: grid;
grid-template-columns: 1.2fr 1fr 1fr 1fr 1.2fr;
gap: 15px;
align-items: center;
font-size: 14px;
}
.detail-header {
color: #84acf0;
border-bottom: 1px solid rgba(132, 172, 240, 0.2);
padding-bottom: 8px;
font-weight: bold;
}
.detail-row {
color: #fff;
/* padding: 6px 0; */
border-bottom: 1px dashed rgba(132, 172, 240, 0.15);
}
.detail-row:hover {
background: rgba(132, 172, 240, 0.1);
border-radius: 4px;
}
/* 已移除:销售额柱状图模块样式 */
.company-item {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.company-bar {
width: 80px;
height: 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
margin-right: 10px;
overflow: hidden;
}
.company-fill {
height: 100%;
border-radius: 6px;
transition: width 0.3s ease;
}
.company-name {
font-size: 10px;
color: #ffffff;
flex: 1;
line-height: 1.2;
}
/* 存栏率模块工具栏与数据说明 */
.panel-tools {
display: flex;
gap: 8px;
margin-left: 10px;
}
.tool-btn {
background: rgba(0, 212, 255, 0.15);
border: 1px solid rgba(0, 212, 255, 0.4);
color: #ffffff;
border-radius: 6px;
padding: 4px 4px;
font-size: 12px;
cursor: pointer;
}
.tool-btn:hover {
background: rgba(0, 212, 255, 0.25);
}
.data-notes {
margin-top: 10px;
min-height: 10%;
display: flex;
flex-direction: column;
gap: 4px;
color: #00d4ff;
font-size: 12px;
}
.data-source {
color: rgba(255, 255, 255, 0.7);
}
</style>