<template>
|
<div class="dashboard-container">
|
<div class="overview-section">
|
<div class="data-overview">
|
<h3>数据总览</h3>
|
<div class="metrics">
|
<div class="metric-item" v-for="(item, index) in dataMetrics" :key="index">
|
<div class="metric-name">{{ item.name }}</div>
|
<div class="metric-value">{{ item.value }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="charts-section">
|
<el-row :gutter="20">
|
<el-col :lg="12">
|
<div class="chart-container">
|
<h3>出库量</h3>
|
<div v-if="loading" class="loading">加载中...</div>
|
<div v-else-if="error" class="error">数据加载失败</div>
|
<div v-else-if="!chartData.outbound.values.length" class="no-data">暂无出库数据</div>
|
<div v-else ref="outboundChart" class="chart"></div>
|
</div>
|
</el-col>
|
<el-col :lg="12">
|
<div class="chart-container">
|
<h3>入库量</h3>
|
<div v-if="loading" class="loading">加载中...</div>
|
<div v-else-if="error" class="error">数据加载失败</div>
|
<div v-else-if="!chartData.inbound.values.length" class="no-data">暂无入库数据</div>
|
<div v-else ref="inboundChart" class="chart"></div>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
<div style="margin-top: 20px;"></div>
|
<div class="charts-section">
|
<el-row :gutter="20">
|
<el-col :lg="24">
|
<div class="chart-container">
|
<h3>月出入库量</h3>
|
<div v-if="loading" class="loading">加载中...</div>
|
<div v-else-if="error" class="error">数据加载失败</div>
|
<div v-else-if="!chartData.monthData.inValue.length || !chartData.monthData.outValue.length" class="no-data">
|
暂无出库数据</div>
|
<div v-else ref="monthDataChart" class="chart1"></div>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import http from '../api/http.js';
|
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
|
import * as echarts from 'echarts';
|
|
// 响应式数据
|
const dataMetrics = ref([]);
|
const chartData = reactive({
|
outbound: { dates: [], values: [] },
|
inbound: { dates: [], values: [] },
|
monthData: { dates: [], inValue: [], outValue: [] }
|
});
|
const loading = ref(true);
|
const error = ref(false);
|
|
// 图表引用和实例
|
const outboundChart = ref(null);
|
const inboundChart = ref(null);
|
const monthDataChart = ref(null);
|
const outboundInstance = ref(null);
|
const inboundInstance = ref(null);
|
const monthDataInstance = ref(null);
|
const charts = ref([]);
|
|
// 初始化图表
|
const initCharts = () => {
|
if (!outboundChart.value || !inboundChart.value || !monthDataChart.value) {
|
console.log('图表容器未找到,延迟初始化');
|
return;
|
}
|
|
outboundInstance.value = echarts.init(outboundChart.value);
|
inboundInstance.value = echarts.init(inboundChart.value);
|
monthDataInstance.value = echarts.init(monthDataChart.value);
|
|
charts.value = [outboundInstance.value, inboundInstance.value, monthDataInstance.value];
|
|
// 出库量图表配置(柱状图)
|
const outboundOption = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'shadow' // 阴影指示器
|
},
|
formatter: function (params) {
|
const data = params[0];
|
return `
|
<div style="font-weight: bold; margin-bottom: 5px;">${data.name}</div>
|
<div style="display: flex; align-items: center;">
|
<span style="display: inline-block; width: 10px; height: 10px; background: ${data.color}; border-radius: 50%; margin-right: 5px;"></span>
|
<span>${data.seriesName}: </span>
|
<span style="font-weight: bold; margin-left: 5px;">${data.value}</span>
|
</div>
|
`;
|
},
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
borderColor: '#ddd',
|
borderWidth: 1,
|
textStyle: {
|
color: '#333'
|
}
|
},
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '3%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
data: chartData.outbound.dates,
|
axisLabel: {
|
rotate: 45
|
}
|
},
|
yAxis: {
|
type: 'value',
|
name: '数量'
|
},
|
series: [{
|
name: '出库量',
|
data: chartData.outbound.values,
|
type: 'bar',
|
itemStyle: {
|
color: '#e06e6e'
|
},
|
barWidth: '60%',
|
animation: true,
|
label: {
|
show: true,
|
position: 'top',
|
formatter: '{c}',
|
color: '#e06e6e'
|
}
|
}]
|
};
|
|
// 入库量图表配置(折线图)
|
const inboundOption = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'line' // 线型指示器
|
},
|
formatter: function (params) {
|
const data = params[0];
|
return `
|
<div style="font-weight: bold; margin-bottom: 5px;">${data.name}</div>
|
<div style="display: flex; align-items: center;">
|
<span style="display: inline-block; width: 10px; height: 10px; background: ${data.color}; border-radius: 50%; margin-right: 5px;"></span>
|
<span>${data.seriesName}: </span>
|
<span style="font-weight: bold; margin-left: 5px;">${data.value}</span>
|
</div>
|
`;
|
},
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
borderColor: '#ddd',
|
borderWidth: 1,
|
textStyle: {
|
color: '#333'
|
}
|
},
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '3%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
data: chartData.inbound.dates,
|
axisLabel: {
|
rotate: 45
|
}
|
},
|
yAxis: {
|
type: 'value',
|
name: '数量'
|
},
|
series: [{
|
name: '入库量',
|
data: chartData.inbound.values,
|
type: 'line',
|
itemStyle: {
|
color: '#4a7bff'
|
},
|
lineStyle: {
|
width: 3
|
},
|
smooth: true,
|
animation: true,
|
label: {
|
show: true,
|
position: 'top',
|
formatter: '{c}',
|
color: '#4a7bff'
|
}
|
}]
|
};
|
|
// 月出入库量图表配置(双折线图)
|
const monthDataOption = {
|
tooltip: {
|
trigger: 'axis',
|
axisPointer: {
|
type: 'cross', // 十字准星指示器
|
crossStyle: {
|
color: '#999'
|
}
|
},
|
formatter: function (params) {
|
let html = `<div style="font-weight: bold; margin-bottom: 5px;">${params[0].name}</div>`;
|
params.forEach(param => {
|
html += `
|
<div style="display: flex; align-items: center; margin: 2px 0;">
|
<span style="display: inline-block; width: 10px; height: 10px; background: ${param.color}; border-radius: 50%; margin-right: 5px;"></span>
|
<span>${param.seriesName}: </span>
|
<span style="font-weight: bold; margin-left: 5px;">${param.value}</span>
|
</div>
|
`;
|
});
|
return html;
|
},
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
borderColor: '#ddd',
|
borderWidth: 1,
|
textStyle: {
|
color: '#333'
|
}
|
},
|
legend: {
|
data: ['入库量', '出库量'],
|
bottom: 0
|
},
|
grid: {
|
left: '3%',
|
right: '4%',
|
bottom: '10%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
data: chartData.monthData.dates,
|
axisLabel: {
|
rotate: 45
|
},
|
axisPointer: {
|
type: 'shadow'
|
}
|
},
|
yAxis: {
|
type: 'value',
|
name: '数量'
|
},
|
series: [
|
{
|
name: '入库量',
|
data: chartData.monthData.inValue,
|
type: 'line',
|
itemStyle: {
|
color: '#4a7bff'
|
},
|
lineStyle: {
|
width: 3
|
},
|
smooth: true,
|
animation: true,
|
label: {
|
show: true,
|
position: 'top',
|
formatter: '{c}',
|
color: '#4a7bff'
|
}
|
},
|
{
|
name: '出库量',
|
data: chartData.monthData.outValue,
|
type: 'line',
|
itemStyle: {
|
color: '#e06e6e'
|
},
|
lineStyle: {
|
width: 3
|
},
|
smooth: true,
|
animation: true,
|
label: {
|
show: true,
|
position: 'top',
|
formatter: '{c}',
|
color: '#e06e6e'
|
}
|
}
|
]
|
};
|
|
outboundInstance.value.setOption(outboundOption);
|
inboundInstance.value.setOption(inboundOption);
|
monthDataInstance.value.setOption(monthDataOption);
|
};
|
|
// 更新图表数据
|
const updateCharts = () => {
|
nextTick(() => {
|
if (outboundInstance.value && chartData.outbound.values.length > 0) {
|
outboundInstance.value.setOption({
|
xAxis: { data: chartData.outbound.dates },
|
series: [{ data: chartData.outbound.values }]
|
});
|
}
|
|
if (inboundInstance.value && chartData.inbound.values.length > 0) {
|
inboundInstance.value.setOption({
|
xAxis: { data: chartData.inbound.dates },
|
series: [{ data: chartData.inbound.values }]
|
});
|
}
|
|
if (monthDataInstance.value && chartData.monthData.dates.length > 0) {
|
monthDataInstance.value.setOption({
|
xAxis: { data: chartData.monthData.dates },
|
series: [
|
{ data: chartData.monthData.inValue },
|
{ data: chartData.monthData.outValue }
|
]
|
});
|
}
|
});
|
};
|
|
// 响应式窗口调整
|
const handleResize = () => {
|
charts.value.forEach(chart => chart && chart.resize());
|
};
|
|
// 数据获取
|
const fetchData = async () => {
|
try {
|
loading.value = true;
|
error.value = false;
|
|
const response = await http.post("api/StockInfo/GetStockData", {});
|
console.log('API响应数据:', response.data);
|
|
if (response.data && response.data.success !== false) {
|
handleDataUpdate(response.data);
|
} else {
|
throw new Error('API返回数据格式错误');
|
}
|
|
loading.value = false;
|
} catch (err) {
|
console.error('API请求失败:', err);
|
loading.value = false;
|
error.value = true;
|
}
|
};
|
|
// 数据处理
|
const handleDataUpdate = (data) => {
|
console.log('处理数据:', data);
|
|
// 更新数据指标
|
if (data.metrics && Array.isArray(data.metrics)) {
|
dataMetrics.value = data.metrics.map(item => ({
|
name: item.name || item.Name || '未知指标',
|
value: item.value != null ? item.value : item.Value || 0
|
}));
|
}
|
|
// 更新图表数据
|
if (data.outbound) {
|
chartData.outbound.dates = data.outbound.dates || [];
|
chartData.outbound.values = data.outbound.values || [];
|
}
|
|
if (data.inbound) {
|
chartData.inbound.dates = data.inbound.dates || [];
|
chartData.inbound.values = data.inbound.values || [];
|
}
|
|
if (data.monthData) {
|
chartData.monthData.dates = data.monthData.dates || [];
|
chartData.monthData.inValue = data.monthData.inValue || [];
|
chartData.monthData.outValue = data.monthData.outValue || [];
|
}
|
|
console.log('更新后的数据指标:', dataMetrics.value);
|
console.log('更新后的图表数据:', chartData);
|
|
// 延迟初始化图表,确保数据已更新
|
nextTick(() => {
|
if (!outboundInstance.value || !inboundInstance.value || !monthDataInstance.value) {
|
initCharts();
|
} else {
|
updateCharts();
|
}
|
});
|
};
|
|
// 轮询控制
|
const intervalId = ref(null);
|
const startPolling = () => {
|
fetchData();
|
intervalId.value = setInterval(fetchData, 5 * 60 * 1000);
|
};
|
|
const stopPolling = () => {
|
if (intervalId.value) {
|
clearInterval(intervalId.value);
|
intervalId.value = null;
|
}
|
};
|
|
// 生命周期
|
onMounted(() => {
|
startPolling();
|
window.addEventListener('resize', handleResize);
|
});
|
|
onUnmounted(() => {
|
stopPolling();
|
charts.value.forEach(chart => chart && chart.dispose());
|
window.removeEventListener('resize', handleResize);
|
});
|
</script>
|
|
<style scoped>
|
.dashboard-container {
|
padding: 20px;
|
background-color: #f5f6fa;
|
min-height: 100vh;
|
}
|
|
.overview-section {
|
display: flex;
|
gap: 20px;
|
margin-bottom: 20px;
|
}
|
|
.data-overview {
|
flex: 1;
|
background: white;
|
padding: 16px;
|
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
}
|
|
.metrics {
|
display: flex;
|
justify-content: space-between;
|
margin-top: 16px;
|
}
|
|
.metric-item {
|
text-align: center;
|
flex: 1;
|
padding: 10px;
|
}
|
|
.metric-name {
|
font-size: 14px;
|
color: #666;
|
margin-bottom: 8px;
|
}
|
|
.metric-value {
|
font-size: 20px;
|
font-weight: bold;
|
margin: 8px 0;
|
color: #333;
|
}
|
|
.charts-section {
|
gap: 20px;
|
}
|
|
.chart-container {
|
flex: 1;
|
background: white;
|
padding: 16px;
|
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
}
|
|
.chart {
|
height: 550px;
|
width: 100%;
|
}
|
|
.chart1 {
|
height: 550px;
|
width: 100%;
|
}
|
|
.loading,
|
.error,
|
.no-data {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
height: 500px;
|
font-size: 18px;
|
}
|
|
.loading {
|
color: #999;
|
}
|
|
.error {
|
color: #2d8cf0;
|
}
|
|
.no-data {
|
color: #909399;
|
}
|
</style>
|