| | |
| | | <template> |
| | | <div class="dashboard-container"> |
| | | <!-- 顶部:本月出入库趋势 (全宽) --> |
| | | <!-- 各仓库月度出入库对比图 --> |
| | | <div class="chart-row full-width"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">本月出入库趋势</div> |
| | | <div id="chart-monthly-trend" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 第二行:今日/本周出入库对比 --> |
| | | <div class="chart-row"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">今日出入库对比</div> |
| | | <div id="chart-today" class="chart-content"></div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="card-title">本周出入库对比</div> |
| | | <div id="chart-week" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 第三行:本月对比/库存总量 --> |
| | | <div class="chart-row"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">本月出入库对比</div> |
| | | <div id="chart-month" class="chart-content"></div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="card-title">当前库存总量</div> |
| | | <div class="stock-total"> |
| | | <div class="total-number">{{ overviewData.TotalStock || 0 }}</div> |
| | | <div class="total-label">托盘</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 第四行:库龄分布/仓库分布 --> |
| | | <div class="chart-row"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">库存库龄分布</div> |
| | | <div id="chart-stock-age" class="chart-content"></div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="card-title">各仓库库存分布</div> |
| | | <div id="chart-warehouse" class="chart-content"></div> |
| | | <div class="card-title">各仓库月度出入库对比</div> |
| | | <div id="chart-warehouse-monthly" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | data() { |
| | | return { |
| | | charts: {}, |
| | | overviewData: { |
| | | TodayInbound: 0, |
| | | TodayOutbound: 0, |
| | | MonthInbound: 0, |
| | | MonthOutbound: 0, |
| | | TotalStock: 0 |
| | | }, |
| | | weeklyData: [], |
| | | monthlyData: [], |
| | | stockAgeData: [], |
| | | warehouseData: [] |
| | | warehouseNames: ['FJSC1', 'ZJSC1', 'GWSC1', 'CWSC1', 'HCSC1'] |
| | | }; |
| | | }, |
| | | mounted() { |
| | |
| | | }, |
| | | |
| | | initCharts() { |
| | | this.charts.monthlyTrend = echarts.init(document.getElementById("chart-monthly-trend")); |
| | | this.charts.today = echarts.init(document.getElementById("chart-today")); |
| | | this.charts.week = echarts.init(document.getElementById("chart-week")); |
| | | this.charts.month = echarts.init(document.getElementById("chart-month")); |
| | | this.charts.stockAge = echarts.init(document.getElementById("chart-stock-age")); |
| | | this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse")); |
| | | this.charts.warehouseMonthly = echarts.init(document.getElementById("chart-warehouse-monthly")); |
| | | }, |
| | | |
| | | async loadData() { |
| | | await this.loadOverview(); |
| | | await this.loadWeeklyStats(); |
| | | await this.loadMonthlyStats(); |
| | | await this.loadStockAgeDistribution(); |
| | | await this.loadStockByWarehouse(); |
| | | }, |
| | | |
| | | async loadOverview() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/Overview"); |
| | | if (res.Status && res.Data) { |
| | | this.overviewData = res.Data; |
| | | this.updateTodayChart(); |
| | | this.updateWeekChart(); |
| | | this.updateMonthChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载总览数据失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadWeeklyStats() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 }); |
| | | if (res.Status && res.Data) { |
| | | this.weeklyData = res.Data; |
| | | this.updateWeekChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载每周统计失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadMonthlyStats() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/MonthlyStats", { months: 12 }); |
| | | if (res.Status && res.Data) { |
| | | this.monthlyData = res.Data; |
| | | this.updateMonthlyTrendChart(); |
| | | } |
| | | const promises = this.warehouseNames.map(warehouse => |
| | | this.http.get("/api/Dashboard/MonthlyStats", { |
| | | months: 6, |
| | | Roadway: warehouse |
| | | }) |
| | | ); |
| | | |
| | | const results = await Promise.all(promises); |
| | | |
| | | this.monthlyData = results.map((res, index) => ({ |
| | | warehouse: this.warehouseNames[index], |
| | | warehouseName: this.getWarehouseName(this.warehouseNames[index]), |
| | | data: res.data || [] |
| | | })); |
| | | |
| | | this.updateWarehouseMonthlyChart(); |
| | | } catch (e) { |
| | | console.error("加载每月统计失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadStockAgeDistribution() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/StockAgeDistribution"); |
| | | if (res.Status && res.Data) { |
| | | this.stockAgeData = res.Data; |
| | | this.updateStockAgeChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载库龄分布失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadStockByWarehouse() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/StockByWarehouse"); |
| | | if (res.Status && res.Data) { |
| | | this.warehouseData = res.Data; |
| | | this.updateWarehouseChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载仓库分布失败", e); |
| | | } |
| | | }, |
| | | |
| | | updateTodayChart() { |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: ["今日"], |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { name: "入库", type: "bar", data: [this.overviewData.TodayInbound], itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "bar", data: [this.overviewData.TodayOutbound], itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | getWarehouseName(code) { |
| | | const nameMap = { |
| | | 'FJSC1': '负极卷1号仓库', |
| | | 'ZJSC1': '正极卷1号仓库', |
| | | 'GWSC1': '高温1号仓库', |
| | | 'CWSC1': '常温1号仓库', |
| | | 'HCSC1': '分容1号仓库' |
| | | }; |
| | | this.charts.today.setOption(option, true); |
| | | return nameMap[code] || code; |
| | | }, |
| | | |
| | | updateWeekChart() { |
| | | const thisWeek = this.getThisWeekData(this.weeklyData); |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: ["本周"], |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { name: "入库", type: "bar", data: [thisWeek.Inbound], itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "bar", data: [thisWeek.Outbound], itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.week.setOption(option, true); |
| | | }, |
| | | |
| | | getThisWeekData(weeklyData) { |
| | | if (!weeklyData || weeklyData.length === 0) return { Inbound: 0, Outbound: 0 }; |
| | | const thisWeekKey = this.getCurrentWeekKey(); |
| | | const thisWeek = weeklyData.find(w => w.Week === thisWeekKey); |
| | | return thisWeek || { Inbound: 0, Outbound: 0 }; |
| | | }, |
| | | |
| | | getCurrentWeekKey() { |
| | | const now = new Date(); |
| | | const diff = (7 + (now.getDay() - 1)) % 7; |
| | | const monday = new Date(now); |
| | | monday.setDate(now.getDate() - diff); |
| | | const year = monday.getFullYear(); |
| | | const jan1 = new Date(year, 0, 1); |
| | | const weekNum = Math.ceil(((monday - jan1) / 86400000 + jan1.getDay() + 1) / 7); |
| | | return `${year}-W${String(weekNum).padStart(2, "0")}`; |
| | | }, |
| | | |
| | | updateMonthChart() { |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: ["本月"], |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { name: "入库", type: "bar", data: [this.overviewData.MonthInbound], itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "bar", data: [this.overviewData.MonthOutbound], itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.month.setOption(option, true); |
| | | }, |
| | | |
| | | updateMonthlyTrendChart() { |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.monthlyData.map(m => m.Month), |
| | | axisLabel: { color: "#fff", rotate: 45 } |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: "value", |
| | | name: "数量", |
| | | axisLabel: { color: "#fff" } |
| | | updateWarehouseMonthlyChart() { |
| | | // 获取所有月份 |
| | | const months = this.monthlyData[0]?.data.map(d => `${d.month}月`) || []; |
| | | |
| | | // 为每个仓库生成系列数据 |
| | | const series = []; |
| | | |
| | | this.monthlyData.forEach((warehouseData, index) => { |
| | | const data = warehouseData.data; |
| | | |
| | | series.push({ |
| | | name: warehouseData.warehouseName, |
| | | type: 'bar', |
| | | data: data.map(d => ({ |
| | | value: (d.inbound || 0) + (d.outbound || 0), |
| | | inbound: d.inbound || 0, |
| | | outbound: d.outbound || 0, |
| | | label: { |
| | | show: true, |
| | | position: 'top', |
| | | formatter: function(params) { |
| | | return `入:${params.data.inbound}\n出:${params.data.outbound}`; |
| | | }, |
| | | fontSize: 10, |
| | | color: '#fff', |
| | | lineHeight: 15 |
| | | } |
| | | })), |
| | | barWidth: '15%', |
| | | barGap: '10%', |
| | | itemStyle: { |
| | | color: this.getBarColor(index), |
| | | borderRadius: [3, 3, 0, 0] |
| | | } |
| | | ], |
| | | series: [ |
| | | { name: "入库", type: "line", data: this.monthlyData.map(m => m.Inbound), itemStyle: { color: "#5470c6" } }, |
| | | { name: "出库", type: "line", data: this.monthlyData.map(m => m.Outbound), itemStyle: { color: "#91cc75" } } |
| | | ] |
| | | }; |
| | | this.charts.monthlyTrend.setOption(option, true); |
| | | }, |
| | | }); |
| | | }); |
| | | |
| | | updateStockAgeChart() { |
| | | const option = { |
| | | tooltip: { trigger: "item" }, |
| | | legend: { data: this.stockAgeData.map(s => s.Range), textStyle: { color: "#fff" } }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: this.stockAgeData.map((s, i) => ({ |
| | | name: s.Range, |
| | | value: s.Count |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)" |
| | | } |
| | | title: { |
| | | text: '各仓库月度出入库对比', |
| | | textStyle: { |
| | | color: '#00ffff', |
| | | fontSize: 16 |
| | | }, |
| | | left: 'center', |
| | | top: 10 |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | }, |
| | | formatter: function(params) { |
| | | let tip = `<strong>${params[0].axisValue}</strong><br/>`; |
| | | params.forEach(param => { |
| | | tip += `<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:5px;"></span>`; |
| | | tip += `${param.seriesName}: `; |
| | | tip += `入库:${param.data.inbound} | 出库:${param.data.outbound} | 总计:${param.value}<br/>`; |
| | | }); |
| | | return tip; |
| | | } |
| | | }, |
| | | legend: { |
| | | data: this.monthlyData.map(d => d.warehouseName), |
| | | textStyle: { color: '#fff', fontSize: 11 }, |
| | | top: 45, |
| | | left: 'center', |
| | | type: 'scroll' |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '10%', |
| | | top: '20%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: months, |
| | | axisLabel: { |
| | | color: '#fff', |
| | | fontSize: 11 |
| | | }, |
| | | axisLine: { |
| | | lineStyle: { color: 'rgba(255,255,255,0.3)' } |
| | | }, |
| | | splitLine: { |
| | | show: true, |
| | | lineStyle: { |
| | | color: 'rgba(255,255,255,0.1)', |
| | | type: 'dashed' |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | this.charts.stockAge.setOption(option, true); |
| | | }, |
| | | |
| | | updateWarehouseChart() { |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.warehouseData.map(w => w.Warehouse), |
| | | axisLabel: { color: "#fff", rotate: 30 } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "bar", |
| | | data: this.warehouseData.map(w => w.Count), |
| | | itemStyle: { color: "#5470c6" } |
| | | type: 'value', |
| | | name: '数量', |
| | | nameTextStyle: { color: '#fff' }, |
| | | axisLabel: { color: '#fff' }, |
| | | splitLine: { |
| | | lineStyle: { |
| | | color: 'rgba(255,255,255,0.1)', |
| | | type: 'dashed' |
| | | } |
| | | } |
| | | ] |
| | | }, |
| | | dataZoom: [ |
| | | { |
| | | type: 'inside', |
| | | start: 0, |
| | | end: 100 |
| | | }, |
| | | { |
| | | start: 0, |
| | | end: 100, |
| | | height: 20, |
| | | bottom: 0, |
| | | borderColor: 'rgba(255,255,255,0.3)', |
| | | fillerColor: 'rgba(0,255,255,0.1)', |
| | | handleStyle: { |
| | | color: '#00ffff', |
| | | borderColor: '#00ffff' |
| | | }, |
| | | textStyle: { |
| | | color: '#fff' |
| | | } |
| | | } |
| | | ], |
| | | series: series |
| | | }; |
| | | this.charts.warehouse.setOption(option, true); |
| | | |
| | | this.charts.warehouseMonthly.setOption(option, true); |
| | | }, |
| | | |
| | | getBarColor(index) { |
| | | const colors = [ |
| | | '#5470c6', // 蓝 |
| | | '#fac858', // 黄 |
| | | '#73c0de', // 天蓝 |
| | | '#fc8452', // 橙 |
| | | '#ea7ccc' // 粉 |
| | | ]; |
| | | return colors[index] || '#5470c6'; |
| | | } |
| | | } |
| | | }; |
| | |
| | | <style scoped> |
| | | .dashboard-container { |
| | | padding: 20px; |
| | | background-color: #0e1a2b; |
| | | color: #e0e0e0; |
| | | min-height: calc(100vh - 60px); |
| | | background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); |
| | | background-attachment: fixed; |
| | | } |
| | | |
| | | .chart-row { |
| | |
| | | |
| | | .chart-card { |
| | | flex: 1; |
| | | background: rgba(255, 255, 255, 0.05); |
| | | border: 1px solid rgba(25, 186, 139, 0.17); |
| | | border-radius: 4px; |
| | | background: rgba(10, 16, 35, 0.6); |
| | | backdrop-filter: blur(10px); |
| | | border: 1px solid rgba(64, 224, 208, 0.3); |
| | | border-radius: 12px; |
| | | padding: 15px; |
| | | position: relative; |
| | | box-shadow: |
| | | 0 0 15px rgba(0, 255, 255, 0.1), |
| | | inset 0 0 10px rgba(64, 224, 208, 0.1); |
| | | transition: all 0.3s ease; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .chart-card:hover { |
| | | transform: translateY(-5px); |
| | | box-shadow: |
| | | 0 0 25px rgba(0, 255, 255, 0.3), |
| | | inset 0 0 15px rgba(64, 224, 208, 0.2); |
| | | border: 1px solid rgba(64, 224, 208, 0.6); |
| | | } |
| | | |
| | | .chart-card::before { |
| | |
| | | left: 0; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-top: 2px solid #02a6b5; |
| | | border-left: 2px solid #02a6b5; |
| | | border-top: 2px solid #00ffff; |
| | | border-left: 2px solid #00ffff; |
| | | box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7); |
| | | } |
| | | |
| | | .chart-card::after { |
| | |
| | | right: 0; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-top: 2px solid #02a6b5; |
| | | border-right: 2px solid #02a6b5; |
| | | border-top: 2px solid #00ffff; |
| | | border-right: 2px solid #00ffff; |
| | | box-shadow: 2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7); |
| | | } |
| | | |
| | | .card-title { |
| | | color: #fff; |
| | | color: #00ffff; |
| | | font-size: 16px; |
| | | text-align: center; |
| | | margin-bottom: 10px; |
| | | text-shadow: 0 0 10px rgba(0, 255, 255, 0.7); |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 280px; |
| | | height: 500px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .stock-total { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 280px; |
| | | } |
| | | |
| | | .total-number { |
| | | font-size: 64px; |
| | | font-weight: bold; |
| | | color: #67caca; |
| | | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif; |
| | | } |
| | | |
| | | .total-label { |
| | | font-size: 18px; |
| | | color: #fcf0d8; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | /* 全宽图表 */ |
| | | .full-width .chart-card { |
| | | flex: none; |
| | | width: 100%; |
| | | } |
| | | |
| | | .full-width .chart-content { |
| | | height: 350px; |
| | | height: 500px; |
| | | } |
| | | </style> |
| | | |
| | | .dashboard-container::before { |
| | | content: ""; |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background-image: |
| | | linear-gradient(rgba(64, 224, 208, 0.05) 1px, transparent 1px), |
| | | linear-gradient(90deg, rgba(64, 224, 208, 0.05) 1px, transparent 1px); |
| | | background-size: 30px 30px; |
| | | pointer-events: none; |
| | | z-index: -1; |
| | | } |
| | | </style> |