| | |
| | | <template> |
| | | <div class="title"></div> |
| | | <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> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { ref, reactive } from 'vue' |
| | | import * as echarts from "echarts"; |
| | | |
| | | export default { |
| | | setup() { |
| | | name: "Home", |
| | | data() { |
| | | return { |
| | | charts: {}, |
| | | overviewData: { |
| | | TodayInbound: 0, |
| | | TodayOutbound: 0, |
| | | MonthInbound: 0, |
| | | MonthOutbound: 0, |
| | | TotalStock: 0 |
| | | }, |
| | | weeklyData: [], |
| | | monthlyData: [], |
| | | stockAgeData: [], |
| | | warehouseData: [] |
| | | }; |
| | | }, |
| | | mounted() { |
| | | this.initCharts(); |
| | | this.loadData(); |
| | | window.addEventListener("resize", this.handleResize); |
| | | }, |
| | | beforeUnmount() { |
| | | window.removeEventListener("resize", this.handleResize); |
| | | Object.values(this.charts).forEach(chart => chart.dispose()); |
| | | }, |
| | | methods: { |
| | | handleResize() { |
| | | Object.values(this.charts).forEach(chart => chart.resize()); |
| | | }, |
| | | |
| | | 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")); |
| | | }, |
| | | |
| | | 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(); |
| | | } |
| | | } 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" } } |
| | | ] |
| | | }; |
| | | this.charts.today.setOption(option, true); |
| | | }, |
| | | |
| | | 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" } |
| | | } |
| | | ], |
| | | series: [ |
| | | { name: "入库", type: "bar", 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: "axis" }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.stockAgeData.map(s => s.Range), |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "bar", |
| | | data: this.stockAgeData.map(s => s.Count), |
| | | itemStyle: { color: "#5470c6" } |
| | | } |
| | | ] |
| | | }; |
| | | 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" } |
| | | } |
| | | ] |
| | | }; |
| | | this.charts.warehouse.setOption(option, true); |
| | | } |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .title { |
| | | line-height: 70vh; |
| | | text-align: center; |
| | | font-size: 28px; |
| | | color: orange; |
| | | .dashboard-container { |
| | | padding: 20px; |
| | | background-color: #0e1a2b; |
| | | min-height: calc(100vh - 60px); |
| | | } |
| | | </style> |
| | | |
| | | .chart-row { |
| | | display: flex; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .chart-row.full-width { |
| | | width: 100%; |
| | | } |
| | | |
| | | .chart-card { |
| | | flex: 1; |
| | | background: rgba(255, 255, 255, 0.05); |
| | | border: 1px solid rgba(25, 186, 139, 0.17); |
| | | border-radius: 4px; |
| | | padding: 15px; |
| | | position: relative; |
| | | } |
| | | |
| | | .chart-card::before { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-top: 2px solid #02a6b5; |
| | | border-left: 2px solid #02a6b5; |
| | | } |
| | | |
| | | .chart-card::after { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 0; |
| | | right: 0; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-top: 2px solid #02a6b5; |
| | | border-right: 2px solid #02a6b5; |
| | | } |
| | | |
| | | .card-title { |
| | | color: #fff; |
| | | font-size: 16px; |
| | | text-align: center; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 280px; |
| | | 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; |
| | | } |
| | | </style> |