|
|
|
@ -126,23 +126,12 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 修复后的产奶目标完成度:环形进度条 + 动态百分比 --> |
|
|
|
<div class="card quota-card"> |
|
|
|
<div class="card-header"> |
|
|
|
<span class="title">🎯 产奶目标完成度</span> |
|
|
|
<span class="title">🐪 泌乳母驼存栏结构</span> |
|
|
|
</div> |
|
|
|
<div class="ring-progress-wrapper"> |
|
|
|
<svg class="ring-progress" viewBox="0 0 120 120"> |
|
|
|
<circle class="ring-bg" cx="60" cy="60" r="54" fill="none" stroke="rgba(52, 152, 219, 0.2)" |
|
|
|
stroke-width="10" /> |
|
|
|
<circle class="ring-fill" cx="60" cy="60" r="54" fill="none" stroke="#5dade2" stroke-width="10" |
|
|
|
stroke-linecap="round" :stroke-dasharray="ringCircumference" :stroke-dashoffset="ringOffset" /> |
|
|
|
<text x="60" y="70" text-anchor="middle" class="ring-percent" fill="#fff" font-size="22" |
|
|
|
font-weight="bold">{{ completionPercent }}%</text> |
|
|
|
<text x="60" y="90" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-size="10">完成率</text> |
|
|
|
</svg> |
|
|
|
</div> |
|
|
|
<div class="quota-footer">年度目标 3500吨 | 已完成 {{ productionStats.totalMilk }} 吨</div> |
|
|
|
<div ref="lactatingBarChart" class="bar-chart-container"></div> |
|
|
|
<div class="quota-footer">泌乳母驼 {{ lactatingCamels }} 峰 | 总存栏 {{ productionStats.camelCount }} 峰</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -154,9 +143,7 @@ import * as echarts from 'echarts'; |
|
|
|
import dayjs from 'dayjs'; |
|
|
|
import 'dayjs/locale/zh-cn'; |
|
|
|
|
|
|
|
// 注意:animate-number 组件需要单独安装或自定义,这里提供一个简单的模拟实现 |
|
|
|
// 如果项目中未安装 vue-animate-number,可以自行实现一个简单的数字动画组件 |
|
|
|
// 为了代码完整性,我们在 components 中注册一个简单的 animate-number 组件 |
|
|
|
// 动画数字组件 |
|
|
|
const AnimateNumber = { |
|
|
|
props: { |
|
|
|
from: Number, |
|
|
|
@ -195,8 +182,8 @@ export default { |
|
|
|
}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
// 默认中心坐标(阿拉善左旗吉兰泰镇附近,与地图实际展示匹配) |
|
|
|
center: { lng: 105.669, lat: 38.833 }, |
|
|
|
// 默认中心坐标 |
|
|
|
center: { lng: 104.516843, lat: 40.272114 }, |
|
|
|
productionStats: { |
|
|
|
totalMilk: 2845, // 吨 |
|
|
|
camelCount: 12860, |
|
|
|
@ -219,36 +206,39 @@ export default { |
|
|
|
], |
|
|
|
camelChartIns: null, |
|
|
|
trendChartIns: null, |
|
|
|
// 环形进度条参数 |
|
|
|
ringCircumference: 2 * Math.PI * 54, // 周长 = 2*pi*r |
|
|
|
// 百度地图实例 |
|
|
|
lactatingBarChartIns: null, |
|
|
|
baiduMap: null |
|
|
|
}; |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
// 完成百分比(基于年度目标3500吨) |
|
|
|
completionPercent() { |
|
|
|
let percent = (this.productionStats.totalMilk / 3500) * 100; |
|
|
|
percent = Math.min(100, Math.max(0, percent)); |
|
|
|
return Math.floor(percent); |
|
|
|
lactatingCamels() { |
|
|
|
return Math.round(this.productionStats.camelCount * 0.48); |
|
|
|
}, |
|
|
|
otherCamels() { |
|
|
|
return this.productionStats.camelCount - this.lactatingCamels; |
|
|
|
}, |
|
|
|
// 环形进度条偏移量 |
|
|
|
ringOffset() { |
|
|
|
const percent = this.completionPercent / 100; |
|
|
|
return this.ringCircumference * (1 - percent); |
|
|
|
barChartData() { |
|
|
|
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; |
|
|
|
const lactatingData = []; |
|
|
|
const otherData = []; |
|
|
|
const baseTotal = this.productionStats.camelCount; |
|
|
|
const ratios = [0.47, 0.46, 0.45, 0.46, 0.48, 0.50, 0.52, 0.51, 0.50, 0.49, 0.48, 0.47]; |
|
|
|
for (let i = 0; i < months.length; i++) { |
|
|
|
const lactating = Math.round(baseTotal * ratios[i]); |
|
|
|
lactatingData.push(lactating); |
|
|
|
otherData.push(baseTotal - lactating); |
|
|
|
} |
|
|
|
return { |
|
|
|
months, |
|
|
|
lactatingData, |
|
|
|
otherData |
|
|
|
}; |
|
|
|
} |
|
|
|
}, |
|
|
|
mounted() { |
|
|
|
this.getCurrentDateTime(); |
|
|
|
this.initAllCharts(); |
|
|
|
window.addEventListener('resize', this.handleResize); |
|
|
|
// 由于百度地图组件 <baidu-map> 需要等待其初始化完成,但实际原生百度地图GL与Vue组件集成可能有问题 |
|
|
|
// 为了保证地图正常显示且与统计指标对齐,我们采用原生方式在 nextTick 中初始化地图,替换原有的 baidu-map 组件标签内容 |
|
|
|
// 注意:template 中的 <baidu-map> 组件可能无法正常工作(需要vue-baidu-map插件),为了可靠性,我们将采用原生方式手动挂载地图 |
|
|
|
// 并且移除 template 中的 baidu-map 组件,改为一个普通的 div 容器,由 js 初始化。 |
|
|
|
// 为保持代码一致性,我将在 mounted 中重新获取容器并初始化原生百度地图,同时修改 template 中的地图容器为普通 div。 |
|
|
|
// 但由于用户代码中使用了 <baidu-map> 标签,可能已安装对应库,为保险,我同时保留两种方式,但根据错误提示,最好改为原生。 |
|
|
|
// 下面将重写地图初始化部分,确保地图正常显示且不超出布局。 |
|
|
|
this.$nextTick(() => { |
|
|
|
this.initBaiduMapNative(); |
|
|
|
}); |
|
|
|
@ -257,19 +247,19 @@ export default { |
|
|
|
window.removeEventListener('resize', this.handleResize); |
|
|
|
if (this.camelChartIns) this.camelChartIns.dispose(); |
|
|
|
if (this.trendChartIns) this.trendChartIns.dispose(); |
|
|
|
if (this.lactatingBarChartIns) this.lactatingBarChartIns.dispose(); |
|
|
|
if (this.baiduMap) { |
|
|
|
// 清理地图 |
|
|
|
this.baiduMap.destroy(); |
|
|
|
} |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
getCurrentDateTime() { |
|
|
|
// 这个函数可以移除,因为原代码中查找 #cloudDate 等元素不存在,但保留无影响 |
|
|
|
// 为了安全,不报错即可 |
|
|
|
// 保留原函数 |
|
|
|
}, |
|
|
|
initAllCharts() { |
|
|
|
this.initCamelStructureChart(); |
|
|
|
this.initTrendChart(); |
|
|
|
this.initLactatingBarChart(); |
|
|
|
}, |
|
|
|
initCamelStructureChart() { |
|
|
|
const chartDom = this.$refs.camelChart; |
|
|
|
@ -332,12 +322,80 @@ export default { |
|
|
|
symbolSize: 8, |
|
|
|
itemStyle: { color: '#3498db' } |
|
|
|
}], |
|
|
|
grid: { containLabel: true, bottom: 10, top: 20, right: 20, left: 45 } |
|
|
|
grid: { containLabel: true, bottom: 0, top: 40, right: 20, left: 10 } |
|
|
|
}); |
|
|
|
}, |
|
|
|
initLactatingBarChart() { |
|
|
|
const chartDom = this.$refs.lactatingBarChart; |
|
|
|
if (!chartDom) return; |
|
|
|
if (this.lactatingBarChartIns) this.lactatingBarChartIns.dispose(); |
|
|
|
this.lactatingBarChartIns = echarts.init(chartDom); |
|
|
|
const { months, lactatingData, otherData } = this.barChartData; |
|
|
|
this.lactatingBarChartIns.setOption({ |
|
|
|
tooltip: { |
|
|
|
trigger: 'axis', |
|
|
|
axisPointer: { type: 'shadow' }, |
|
|
|
backgroundColor: 'rgba(0,0,0,0.7)', |
|
|
|
borderColor: '#3498db', |
|
|
|
textStyle: { color: '#fff' }, |
|
|
|
formatter: (params) => { |
|
|
|
let res = `${params[0].axisValue}<br/>`; |
|
|
|
res += `${params[0].marker} 泌乳母驼: ${params[0].value} 峰<br/>`; |
|
|
|
res += `${params[1].marker} 其他骆驼: ${params[1].value} 峰<br/>`; |
|
|
|
res += `合计: ${params[0].value + params[1].value} 峰`; |
|
|
|
return res; |
|
|
|
} |
|
|
|
}, |
|
|
|
legend: { |
|
|
|
data: ['泌乳母驼', '其他骆驼'], |
|
|
|
textStyle: { color: '#fff' }, |
|
|
|
left: 'left', |
|
|
|
itemWidth: 25, |
|
|
|
itemHeight: 14 |
|
|
|
}, |
|
|
|
grid: { |
|
|
|
containLabel: true, |
|
|
|
bottom: 15, |
|
|
|
top: 30, |
|
|
|
right: 15, |
|
|
|
left: 50 |
|
|
|
}, |
|
|
|
xAxis: { |
|
|
|
type: 'category', |
|
|
|
data: months, |
|
|
|
axisLabel: { color: '#fff', rotate: 30, interval: 0 }, |
|
|
|
axisLine: { lineStyle: { color: '#5dade2' } }, |
|
|
|
axisTick: { show: false } |
|
|
|
}, |
|
|
|
yAxis: { |
|
|
|
type: 'value', |
|
|
|
nameTextStyle: { color: '#fff' }, |
|
|
|
axisLabel: { color: '#fff' }, |
|
|
|
splitLine: { lineStyle: { color: 'rgba(93, 173, 226, 0.3)' } }, |
|
|
|
axisLine: { lineStyle: { color: '#5dade2' } } |
|
|
|
}, |
|
|
|
series: [ |
|
|
|
{ |
|
|
|
name: '泌乳母驼', |
|
|
|
type: 'bar', |
|
|
|
stack: 'total', |
|
|
|
data: lactatingData, |
|
|
|
itemStyle: { color: '#3498db', borderRadius: [4, 4, 0, 0] }, |
|
|
|
label: { show: false } |
|
|
|
}, |
|
|
|
{ |
|
|
|
name: '其他骆驼', |
|
|
|
type: 'bar', |
|
|
|
stack: 'total', |
|
|
|
data: otherData, |
|
|
|
itemStyle: { color: '#85c1e9', borderRadius: [4, 4, 0, 0] }, |
|
|
|
label: { show: false } |
|
|
|
} |
|
|
|
], |
|
|
|
backgroundColor: 'transparent' |
|
|
|
}); |
|
|
|
}, |
|
|
|
// 原生百度地图GL初始化 (替换原有组件,确保地图显示且布局对齐) |
|
|
|
initBaiduMapNative() { |
|
|
|
// 检查百度地图API是否加载完成 |
|
|
|
if (typeof window.BMapGL === 'undefined') { |
|
|
|
console.error('百度地图API未加载,请检查AK或网络,稍后重试'); |
|
|
|
setTimeout(() => this.initBaiduMapNative(), 500); |
|
|
|
@ -348,23 +406,16 @@ export default { |
|
|
|
console.error('地图容器未找到'); |
|
|
|
return; |
|
|
|
} |
|
|
|
// 清空容器可能存在的子元素 |
|
|
|
mapContainer.innerHTML = ''; |
|
|
|
|
|
|
|
// 创建地图实例 (阿拉善左旗吉兰泰镇附近坐标) |
|
|
|
const map = new window.BMapGL.Map(mapContainer); |
|
|
|
const centerPoint = new window.BMapGL.Point(105.669, 38.833); |
|
|
|
map.centerAndZoom(centerPoint, 11); |
|
|
|
map.enableScrollWheelZoom(true); |
|
|
|
// 设置深色地图样式 |
|
|
|
map.setMapStyle({ style: 'midnight' }); |
|
|
|
|
|
|
|
// 添加标记点示例:展示核心牧场位置 |
|
|
|
const markerPoint = new window.BMapGL.Point(105.669, 38.833); |
|
|
|
const marker = new window.BMapGL.Marker(markerPoint); |
|
|
|
map.addOverlay(marker); |
|
|
|
|
|
|
|
// 添加信息窗口 |
|
|
|
const infoWindow = new window.BMapGL.InfoWindow( |
|
|
|
'<div style="background:#0a1a2e;color:#fff;padding:5px;border-radius:8px;"><strong>吉兰泰骆驼产业园</strong><br/>存栏骆驼: 4200峰<br/>日产奶量: 8.6吨</div>', |
|
|
|
{ width: 200, offset: new window.BMapGL.Size(0, -30) } |
|
|
|
@ -373,7 +424,6 @@ export default { |
|
|
|
map.openInfoWindow(infoWindow, markerPoint); |
|
|
|
}); |
|
|
|
|
|
|
|
// 添加区域示意圈 |
|
|
|
const circlePoint = new window.BMapGL.Point(105.62, 38.81); |
|
|
|
const circle = new window.BMapGL.Circle(circlePoint, 5000, { |
|
|
|
strokeColor: "#3498db", |
|
|
|
@ -383,7 +433,6 @@ export default { |
|
|
|
}); |
|
|
|
map.addOverlay(circle); |
|
|
|
|
|
|
|
// 添加文本标签 |
|
|
|
const labelOpts = { position: new window.BMapGL.Point(105.69, 38.85), offset: new window.BMapGL.Size(20, 0) }; |
|
|
|
const label = new window.BMapGL.Label('巴音塔拉嘎查', labelOpts); |
|
|
|
label.setStyle({ |
|
|
|
@ -396,7 +445,6 @@ export default { |
|
|
|
}); |
|
|
|
map.addOverlay(label); |
|
|
|
|
|
|
|
// 添加第二个标注点 |
|
|
|
const markerPoint2 = new window.BMapGL.Point(105.59, 38.79); |
|
|
|
const marker2 = new window.BMapGL.Marker(markerPoint2); |
|
|
|
map.addOverlay(marker2); |
|
|
|
@ -409,8 +457,6 @@ export default { |
|
|
|
}); |
|
|
|
|
|
|
|
this.baiduMap = map; |
|
|
|
|
|
|
|
// 强制resize确保地图容器正确渲染 |
|
|
|
setTimeout(() => { |
|
|
|
if (this.baiduMap) { |
|
|
|
this.baiduMap.centerAndZoom(centerPoint, 11); |
|
|
|
@ -420,6 +466,7 @@ export default { |
|
|
|
handleResize() { |
|
|
|
if (this.camelChartIns) this.camelChartIns.resize(); |
|
|
|
if (this.trendChartIns) this.trendChartIns.resize(); |
|
|
|
if (this.lactatingBarChartIns) this.lactatingBarChartIns.resize(); |
|
|
|
if (this.baiduMap) { |
|
|
|
setTimeout(() => { |
|
|
|
this.baiduMap.centerAndZoom(this.baiduMap.getCenter(), this.baiduMap.getZoom()); |
|
|
|
@ -431,7 +478,6 @@ export default { |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped lang="scss"> |
|
|
|
// 修复全局滚动和高度问题,确保底部内容显示完整 |
|
|
|
.production-dashboard { |
|
|
|
width: 100%; |
|
|
|
height: 100vh; |
|
|
|
@ -446,8 +492,7 @@ export default { |
|
|
|
.dashboard-main { |
|
|
|
display: flex; |
|
|
|
gap: 20px; |
|
|
|
flex-wrap: nowrap; /* 改为不换行,确保三列并排对齐,超出则滚动 */ |
|
|
|
align-items: stretch; /* 使各列高度一致 */ |
|
|
|
align-items: stretch; /* 确保所有列高度一致 */ |
|
|
|
|
|
|
|
.col-left, |
|
|
|
.col-right { |
|
|
|
@ -467,7 +512,6 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* 响应式:当屏幕宽度较小时,允许换行滚动,但保持对齐 */ |
|
|
|
@media (max-width: 1200px) { |
|
|
|
.dashboard-main { |
|
|
|
flex-wrap: wrap; |
|
|
|
@ -486,6 +530,7 @@ export default { |
|
|
|
transition: all 0.3s ease; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
flex: 1; /* 让卡片自动填充可用高度 */ |
|
|
|
|
|
|
|
&:hover { |
|
|
|
border-color: #5dade2; |
|
|
|
@ -516,15 +561,10 @@ export default { |
|
|
|
font-size: 12px; |
|
|
|
color: white; |
|
|
|
} |
|
|
|
|
|
|
|
.sub { |
|
|
|
font-size: 11px; |
|
|
|
color: rgba(255, 255, 255, 0.7); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 地图上方的统计指标 |
|
|
|
/* 地图上方统计指标 */ |
|
|
|
.map-stats { |
|
|
|
display: flex; |
|
|
|
gap: 12px; |
|
|
|
@ -536,7 +576,7 @@ export default { |
|
|
|
background: rgba(0, 0, 0, 0.3); |
|
|
|
backdrop-filter: blur(4px); |
|
|
|
border-radius: 12px; |
|
|
|
padding: 10px 8px; |
|
|
|
padding: 24px 8px; |
|
|
|
text-align: center; |
|
|
|
border: 1.5px solid rgba(52, 152, 219, 0.6); |
|
|
|
transition: all 0.2s; |
|
|
|
@ -548,14 +588,15 @@ export default { |
|
|
|
|
|
|
|
.stat-label { |
|
|
|
display: block; |
|
|
|
font-size: 11px; |
|
|
|
color: rgba(255, 255, 255, 0.8); |
|
|
|
margin-bottom: 6px; |
|
|
|
font-size: 20px; |
|
|
|
color: rgba(255, 255, 255, 0.85); |
|
|
|
margin-bottom: 10px; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
.stat-number { |
|
|
|
display: block; |
|
|
|
font-size: 22px; |
|
|
|
font-size: 28px; |
|
|
|
font-weight: bold; |
|
|
|
color: #5dade2; |
|
|
|
|
|
|
|
@ -571,6 +612,7 @@ export default { |
|
|
|
grid-template-columns: repeat(2, 1fr); |
|
|
|
gap: 16px; |
|
|
|
padding: 20px; |
|
|
|
flex: 1; |
|
|
|
|
|
|
|
.kpi-item { |
|
|
|
background: rgba(0, 0, 0, 0.25); |
|
|
|
@ -579,6 +621,9 @@ export default { |
|
|
|
text-align: center; |
|
|
|
border: 1.5px solid rgba(52, 152, 219, 0.5); |
|
|
|
transition: all 0.2s; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
justify-content: center; |
|
|
|
|
|
|
|
&:hover { |
|
|
|
border-color: #5dade2; |
|
|
|
@ -619,14 +664,21 @@ export default { |
|
|
|
width: 100%; |
|
|
|
height: 220px; |
|
|
|
padding: 8px; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.bar-chart-container { |
|
|
|
width: 100%; |
|
|
|
height: 260px; |
|
|
|
padding: 8px 8px 0 8px; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
/* 百度地图容器样式 - 关键修复高度自适应且不超出 */ |
|
|
|
.map-card { |
|
|
|
margin-top: 0; |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
min-height: 500px; /* 保证地图有足够高度,同时响应式 */ |
|
|
|
min-height: 500px; |
|
|
|
|
|
|
|
.baidu-map-container { |
|
|
|
width: 100%; |
|
|
|
@ -642,7 +694,7 @@ export default { |
|
|
|
.alert-list, |
|
|
|
.task-list { |
|
|
|
padding: 8px 16px; |
|
|
|
max-height: 260px; |
|
|
|
flex: 1; |
|
|
|
overflow-y: auto; |
|
|
|
|
|
|
|
.alert-item, |
|
|
|
@ -722,51 +774,38 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* 环形进度条样式 */ |
|
|
|
.ring-progress-wrapper { |
|
|
|
display: flex; |
|
|
|
justify-content: center; |
|
|
|
align-items: center; |
|
|
|
padding: 20px 0 10px; |
|
|
|
} |
|
|
|
|
|
|
|
.ring-progress { |
|
|
|
width: 130px; |
|
|
|
height: 130px; |
|
|
|
transform: rotate(-90deg); |
|
|
|
} |
|
|
|
|
|
|
|
.ring-bg { |
|
|
|
stroke: rgba(52, 152, 219, 0.25); |
|
|
|
} |
|
|
|
|
|
|
|
.ring-fill { |
|
|
|
stroke: #5dade2; |
|
|
|
transition: stroke-dashoffset 0.8s ease; |
|
|
|
filter: drop-shadow(0 0 5px rgba(93, 173, 226, 0.5)); |
|
|
|
} |
|
|
|
|
|
|
|
.ring-percent { |
|
|
|
dominant-baseline: middle; |
|
|
|
font-size: 22px; |
|
|
|
fill: #fff; |
|
|
|
} |
|
|
|
|
|
|
|
.quota-footer { |
|
|
|
text-align: center; |
|
|
|
font-size: 12px; |
|
|
|
padding: 12px 0 18px; |
|
|
|
color: rgba(255, 255, 255, 0.7); |
|
|
|
border-top: 1px solid rgba(52, 152, 219, 0.4); |
|
|
|
margin-top: 4px; |
|
|
|
margin-top: auto; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
// 确保右侧卡片内容不溢出,滚动条美观 |
|
|
|
.col-right .card { |
|
|
|
max-height: 100%; |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
/* 确保左侧卡片内容自适应 */ |
|
|
|
.col-left .card { |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
|
|
|
|
.col-left .kpi-card { |
|
|
|
flex: 0 0 auto; /* KPI卡片不自动拉伸 */ |
|
|
|
} |
|
|
|
|
|
|
|
.col-left .chart-card { |
|
|
|
flex: 1; |
|
|
|
min-height: 240px; |
|
|
|
} |
|
|
|
|
|
|
|
// 修复整体滚动时底部内容可见 |
|
|
|
.production-dashboard::-webkit-scrollbar { |
|
|
|
width: 6px; |
|
|
|
height: 6px; |
|
|
|
|