| | |
| | | <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> |
| | | <!-- 顶部KPI卡片:显示总货位及各仓库货位 --> |
| | | <div class="kpi-cards"> |
| | | <div class="kpi-card"> |
| | | <div class="kpi-icon">🏚️</div> |
| | | <div class="kpi-info"> |
| | | <div class="kpi-label">总货位</div> |
| | | <div class="kpi-value">{{ totalLocation }}</div> |
| | | </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 class="kpi-card"> |
| | | <div class="kpi-icon">🔥</div> |
| | | <div class="kpi-info"> |
| | | <div class="kpi-label">化成库</div> |
| | | <div class="kpi-value">{{ warehouseLocations.hc }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="card-title">本周出入库对比</div> |
| | | <div id="chart-week" class="chart-content"></div> |
| | | <div class="kpi-card"> |
| | | <div class="kpi-icon">🌡️</div> |
| | | <div class="kpi-info"> |
| | | <div class="kpi-label">高温库</div> |
| | | <div class="kpi-value">{{ warehouseLocations.gw }}</div> |
| | | </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 class="kpi-card"> |
| | | <div class="kpi-icon">❄️</div> |
| | | <div class="kpi-info"> |
| | | <div class="kpi-label">常温库</div> |
| | | <div class="kpi-value">{{ warehouseLocations.cw }}</div> |
| | | </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 class="kpi-card"> |
| | | <div class="kpi-icon">📜</div> |
| | | <div class="kpi-info"> |
| | | <div class="kpi-label">极卷库</div> |
| | | <div class="kpi-value">{{ warehouseLocations.jj }}</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> |
| | | <!-- 第一行:4个每日出入库趋势图(每行2个) --> |
| | | <div class="chart-row daily-grid"> |
| | | <div class="chart-card" v-for="warehouse in dailyWarehouses" :key="warehouse.code"> |
| | | <div class="card-title">{{ warehouse.name }} - 每日趋势</div> |
| | | <!-- 仓库数字显示区域:显示每日总量和空托盘数量 --> |
| | | <div class="warehouse-numbers"> |
| | | <!-- 极卷库显示有货托盘(电池数量)和空托盘 --> |
| | | <template v-if="warehouse.code === 'ROLL'"> |
| | | <div class="number-item battery"> |
| | | <span class="number-label">电池数量</span> |
| | | <span class="number-value">{{ getBatteryCount(warehouse.code) }}</span> |
| | | </div> |
| | | <div class="number-item empty-tray"> |
| | | <span class="number-label">空托盘数量</span> |
| | | <span class="number-value">{{ getEmptyTrayCount(warehouse.code) }}</span> |
| | | </div> |
| | | </template> |
| | | <!-- 其他仓库显示电池数量和空托盘数量 --> |
| | | <template v-else> |
| | | <div class="number-item inbound"> |
| | | <span class="number-label">电池数量</span> |
| | | <span class="number-value">{{ getBatteryCount(warehouse.code) }}</span> |
| | | </div> |
| | | <div class="number-item empty-tray"> |
| | | <span class="number-label">空托盘数量</span> |
| | | <span class="number-value">{{ getEmptyTrayCount(warehouse.code) }}</span> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | <div :id="`daily-chart-${warehouse.code}`" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 仓库分布(柱状图,显示各仓库已用/剩余容量) --> |
| | | <div class="chart-row"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">各仓库库存分布</div> |
| | | <div id="chart-warehouse" class="chart-content"></div> |
| | |
| | | data() { |
| | | return { |
| | | charts: {}, |
| | | overviewData: { |
| | | TodayInbound: 0, |
| | | TodayOutbound: 0, |
| | | MonthInbound: 0, |
| | | MonthOutbound: 0, |
| | | TotalStock: 0 |
| | | // 四个核心仓库(合并了正负极卷库为极卷库) |
| | | dailyWarehouses: [ |
| | | { code: "HCSC1", name: "化成库", type: "hc" }, |
| | | { code: "GWSC1", name: "高温库", type: "gw" }, |
| | | { code: "CWSC1", name: "常温库", type: "cw" }, |
| | | { code: "ROLL", name: "极卷库", type: "jj" } |
| | | ], |
| | | // 原始每日数据存储 (其中ROLL为合并后的极卷库数据) |
| | | dailyDataMap: { |
| | | GWSC1: [], |
| | | CWSC1: [], |
| | | HCSC1: [], |
| | | ROLL: [] |
| | | }, |
| | | weeklyData: [], |
| | | monthlyData: [], |
| | | stockAgeData: [], |
| | | warehouseData: [] |
| | | // 存储每个仓库的电池数量 |
| | | warehouseStocks: { |
| | | GWSC1: 0, |
| | | CWSC1: 0, |
| | | HCSC1: 0, |
| | | ROLL: 0 |
| | | }, |
| | | // 极卷库特殊数据 |
| | | rollData: { |
| | | batteryCount: 0, // 电池数量 |
| | | emptyTrayCount: 0 // 空托盘数量 |
| | | }, |
| | | // 其他仓库的空托盘数量 |
| | | emptyTrayCounts: { |
| | | GWSC1: 0, |
| | | CWSC1: 0, |
| | | HCSC1: 0 |
| | | }, |
| | | warehouseData: [], // 仓库分布图数据 |
| | | // 仓库货位数据(固定配置) |
| | | warehouseLocations: { |
| | | hc: 35, // 化成库 |
| | | gw: 324, // 高温库 |
| | | cw: 140, // 常温库 |
| | | jj: 104 // 极卷库 |
| | | } |
| | | }; |
| | | }, |
| | | computed: { |
| | | // 总货位计算 |
| | | totalLocation() { |
| | | return this.warehouseLocations.hc + this.warehouseLocations.gw + |
| | | this.warehouseLocations.cw + this.warehouseLocations.jj; |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.initCharts(); |
| | |
| | | }, |
| | | beforeUnmount() { |
| | | window.removeEventListener("resize", this.handleResize); |
| | | Object.values(this.charts).forEach(chart => chart.dispose()); |
| | | Object.values(this.charts).forEach(chart => chart && chart.dispose()); |
| | | }, |
| | | methods: { |
| | | handleResize() { |
| | | Object.values(this.charts).forEach(chart => chart.resize()); |
| | | Object.values(this.charts).forEach(chart => 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.dailyWarehouses.forEach(warehouse => { |
| | | const chartId = `daily-chart-${warehouse.code}`; |
| | | const el = document.getElementById(chartId); |
| | | if (el) { |
| | | this.charts[`daily-${warehouse.code}`] = echarts.init(el); |
| | | } |
| | | }); |
| | | // 初始化仓库分布图表 |
| | | 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"); |
| | | console.log("总览数据", res.Data); |
| | | if (res.Status && res.Data) { |
| | | this.overviewData = res.Data; |
| | | this.updateTodayChart(); |
| | | this.updateWeekChart(); |
| | | this.updateMonthChart(); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载总览数据失败", e); |
| | | // 并行加载所有数据 |
| | | await Promise.all([ |
| | | this.loadAllDailyStats(), |
| | | this.loadStockAndTrayCount(), |
| | | this.loadStockByWarehouse() |
| | | ]); |
| | | |
| | | // 更新所有图表 |
| | | this.updateAllDailyCharts(); |
| | | } catch (error) { |
| | | console.error("加载数据失败:", error); |
| | | this.$message?.error("数据加载失败,请稍后重试"); |
| | | } |
| | | }, |
| | | |
| | | 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 loadAllDailyStats() { |
| | | console.log("正在加载所有仓库的每日统计数据..."); |
| | | const res = await this.http.get("/api/Dashboard/DailyStats?days=10"); |
| | | if (res.status && res.data) { |
| | | console.log("所有仓库每日统计数据:", res.data); |
| | | |
| | | const dataArray = res.data; |
| | | |
| | | // 按仓库分类数据 |
| | | dataArray.forEach(item => { |
| | | const roadway = item.roadway; |
| | | const dailyStats = item.dailyStats || []; |
| | | |
| | | switch(roadway) { |
| | | case "GWSC1": |
| | | this.dailyDataMap.GWSC1 = dailyStats; |
| | | break; |
| | | case "CWSC1": |
| | | this.dailyDataMap.CWSC1 = dailyStats; |
| | | break; |
| | | case "HCSC1": |
| | | this.dailyDataMap.HCSC1 = dailyStats; |
| | | break; |
| | | case "ZJSC1": |
| | | case "FJSC1": |
| | | // 极卷库数据合并处理 |
| | | this.mergeRollDailyStats(dailyStats); |
| | | break; |
| | | } |
| | | }); |
| | | |
| | | console.log("GWSC1数据:", this.dailyDataMap.GWSC1); |
| | | console.log("CWSC1数据:", this.dailyDataMap.CWSC1); |
| | | console.log("HCSC1数据:", this.dailyDataMap.HCSC1); |
| | | console.log("极卷库合并数据:", this.dailyDataMap.ROLL); |
| | | } else { |
| | | console.error("获取每日数据失败"); |
| | | } |
| | | }, |
| | | |
| | | 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); |
| | | mergeRollDailyStats(newData) { |
| | | // 合并正极卷库和负极卷库的每日数据 |
| | | if (!this.dailyDataMap.ROLL.length) { |
| | | this.dailyDataMap.ROLL = [...newData]; |
| | | } else { |
| | | const dateMap = new Map(); |
| | | [...this.dailyDataMap.ROLL, ...newData].forEach(item => { |
| | | const date = item.date; |
| | | if (date) { |
| | | if (!dateMap.has(date)) { |
| | | dateMap.set(date, { date, inbound: 0, outbound: 0 }); |
| | | } |
| | | const existing = dateMap.get(date); |
| | | existing.inbound += item.inbound || 0; |
| | | existing.outbound += item.outbound || 0; |
| | | } |
| | | }); |
| | | |
| | | const sortedDates = Array.from(dateMap.keys()).sort(); |
| | | const mergedData = sortedDates.map(date => dateMap.get(date)); |
| | | this.dailyDataMap["ROLL"] = mergedData; |
| | | } |
| | | }, |
| | | |
| | | 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 loadStockAndTrayCount() { |
| | | // 加载电池数量和空托盘数量 |
| | | console.log("正在加载电池数量和空托盘数据..."); |
| | | const res = await this.http.get("/api/Dashboard/StockAndTrayCount"); |
| | | |
| | | if (res.status && res.data) { |
| | | console.log("电池和空托盘数据:", res.data); |
| | | |
| | | // 重置数据 |
| | | this.rollData.batteryCount = 0; |
| | | this.rollData.emptyTrayCount = 0; |
| | | |
| | | // 根据返回的数据结构解析 |
| | | res.data.forEach(item => { |
| | | const warehouseName = item.warehouseName; |
| | | const batteryCount = item.batteryCount || 0; |
| | | const emptyTrayCount = item.emptyTrayCount || 0; |
| | | |
| | | // 根据仓库名称映射到对应的代码 |
| | | if (warehouseName === "高温库") { |
| | | this.emptyTrayCounts.GWSC1 = emptyTrayCount; |
| | | this.warehouseStocks.GWSC1 = batteryCount; |
| | | } else if (warehouseName === "常温库") { |
| | | this.emptyTrayCounts.CWSC1 = emptyTrayCount; |
| | | this.warehouseStocks.CWSC1 = batteryCount; |
| | | } else if (warehouseName === "化成库") { |
| | | this.emptyTrayCounts.HCSC1 = emptyTrayCount; |
| | | this.warehouseStocks.HCSC1 = batteryCount; |
| | | } else if (warehouseName === "极卷库") { |
| | | // 极卷库需要合并两个极卷库的数据 |
| | | this.rollData.batteryCount += batteryCount; |
| | | this.rollData.emptyTrayCount += emptyTrayCount; |
| | | } |
| | | }); |
| | | |
| | | // 设置极卷库总电池数量 |
| | | this.warehouseStocks.ROLL = this.rollData.batteryCount; |
| | | |
| | | console.log("特殊数据加载完成:", { |
| | | rollData: this.rollData, |
| | | emptyTrayCounts: this.emptyTrayCounts, |
| | | warehouseStocks: this.warehouseStocks |
| | | }); |
| | | } else { |
| | | console.error("获取电池和空托盘数据失败"); |
| | | throw new Error("获取电池和空托盘数据失败"); |
| | | } |
| | | }, |
| | | |
| | | async loadStockByWarehouse() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/StockByWarehouse"); |
| | | if (res.Status && res.Data) { |
| | | this.warehouseData = res.Data; |
| | | this.updateWarehouseChart(); |
| | | console.log("正在加载仓库分布数据..."); |
| | | const res = await this.http.get("/api/Dashboard/StockByWarehouse"); |
| | | |
| | | if (res.status && res.data) { |
| | | console.log("仓库分布数据:", res.data); |
| | | const rawData = res.data.data || res.data; |
| | | |
| | | // 处理极卷库合并 |
| | | let rollHasStock = 0; |
| | | let rollNoStock = 0; |
| | | let rollTotal = 0; |
| | | const otherWarehouses = []; |
| | | |
| | | rawData.forEach(item => { |
| | | if (item.warehouse === "极卷库" || item.warehouse.includes("极卷库")) { |
| | | // 合并极卷库数据 |
| | | rollHasStock += item.hasStock || 0; |
| | | rollNoStock += item.noStock || 0; |
| | | rollTotal += item.total || 0; |
| | | } else { |
| | | otherWarehouses.push(item); |
| | | } |
| | | }); |
| | | |
| | | // 添加合并后的极卷库 |
| | | if (rollTotal > 0) { |
| | | const hasStockPercentage = ((rollHasStock / rollTotal) * 100).toFixed(1) + "%"; |
| | | const noStockPercentage = ((rollNoStock / rollTotal) * 100).toFixed(1) + "%"; |
| | | |
| | | otherWarehouses.push({ |
| | | warehouse: "极卷库", |
| | | hasStock: rollHasStock, |
| | | noStock: rollNoStock, |
| | | total: rollTotal, |
| | | hasStockPercentage: hasStockPercentage, |
| | | noStockPercentage: noStockPercentage |
| | | }); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载仓库分布失败", e); |
| | | |
| | | this.warehouseData = otherWarehouses; |
| | | this.updateWarehouseChart(); |
| | | } else { |
| | | console.error("获取仓库分布数据失败"); |
| | | throw new Error("获取仓库分布数据失败"); |
| | | } |
| | | }, |
| | | |
| | | 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); |
| | | getDailyTotalInbound(warehouseCode) { |
| | | const data = this.dailyDataMap[warehouseCode] || []; |
| | | return data.reduce((sum, item) => sum + (item.inbound || 0), 0); |
| | | }, |
| | | |
| | | 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); |
| | | getDailyTotalOutbound(warehouseCode) { |
| | | const data = this.dailyDataMap[warehouseCode] || []; |
| | | return data.reduce((sum, item) => sum + (item.outbound || 0), 0); |
| | | }, |
| | | |
| | | 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 }; |
| | | getWarehouseStock(warehouseCode) { |
| | | return this.warehouseStocks[warehouseCode] || 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")}`; |
| | | getBatteryCount(warehouseCode) { |
| | | if (warehouseCode === 'ROLL') { |
| | | return this.rollData.batteryCount; |
| | | } else if (warehouseCode === 'GWSC1') { |
| | | return this.warehouseStocks.GWSC1; |
| | | } else if (warehouseCode === 'CWSC1') { |
| | | return this.warehouseStocks.CWSC1; |
| | | } else if (warehouseCode === 'HCSC1') { |
| | | return this.warehouseStocks.HCSC1; |
| | | } |
| | | return 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); |
| | | getEmptyTrayCount(warehouseCode) { |
| | | if (warehouseCode === 'ROLL') { |
| | | return this.rollData.emptyTrayCount; |
| | | } else if (warehouseCode === 'GWSC1') { |
| | | return this.emptyTrayCounts.GWSC1; |
| | | } else if (warehouseCode === 'CWSC1') { |
| | | return this.emptyTrayCounts.CWSC1; |
| | | } else if (warehouseCode === 'HCSC1') { |
| | | return this.emptyTrayCounts.HCSC1; |
| | | } |
| | | return 0; |
| | | }, |
| | | |
| | | 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" } |
| | | updateAllDailyCharts() { |
| | | this.dailyWarehouses.forEach(warehouse => { |
| | | this.updateDailyChartForWarehouse(warehouse.code); |
| | | }); |
| | | }, |
| | | |
| | | updateDailyChartForWarehouse(roadway) { |
| | | const chart = this.charts[`daily-${roadway}`]; |
| | | if (!chart) return; |
| | | |
| | | const data = this.dailyDataMap[roadway] || []; |
| | | |
| | | // 如果没有数据,显示空图表提示 |
| | | if (!data.length) { |
| | | chart.setOption({ |
| | | title: { |
| | | show: true, |
| | | text: '暂无数据', |
| | | left: 'center', |
| | | top: 'center', |
| | | textStyle: { color: '#ccc', fontSize: 14 } |
| | | } |
| | | ], |
| | | 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); |
| | | }, |
| | | }, true); |
| | | return; |
| | | } |
| | | |
| | | updateStockAgeChart() { |
| | | const dates = data.map(d => d.date); |
| | | const inboundData = data.map(d => d.inbound || 0); |
| | | const outboundData = data.map(d => d.outbound || 0); |
| | | |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: function(params) { |
| | | let result = params[0].axisValue + "<br/>"; |
| | | params.forEach(p => { |
| | | result += `${p.marker}${p.seriesName}: ${p.value}<br/>`; |
| | | }); |
| | | return result; |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ["入库", "出库"], |
| | | textStyle: { color: "#fff" }, |
| | | top: 0, |
| | | right: 10, |
| | | itemWidth: 20, |
| | | itemHeight: 12 |
| | | }, |
| | | grid: { |
| | | left: "8%", |
| | | right: "8%", |
| | | top: "18%", |
| | | bottom: "12%", |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.stockAgeData.map(s => s.Range), |
| | | axisLabel: { color: "#fff" } |
| | | data: dates, |
| | | axisLabel: { |
| | | color: "#ccc", |
| | | rotate: 45, |
| | | fontSize: 10, |
| | | interval: 0, |
| | | margin: 8 |
| | | }, |
| | | axisLine: { lineStyle: { color: "#4a5b6e" } } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | name: "数量", |
| | | nameTextStyle: { color: "#ccc", fontSize: 11 }, |
| | | axisLabel: { color: "#ccc" }, |
| | | splitLine: { lineStyle: { color: "#2a3a4a", type: "dashed" } } |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "入库", |
| | | type: "bar", |
| | | data: this.stockAgeData.map(s => s.Count), |
| | | itemStyle: { color: "#5470c6" } |
| | | data: inboundData, |
| | | itemStyle: { |
| | | color: "#5470c6", |
| | | borderRadius: [4, 4, 0, 0] |
| | | }, |
| | | barWidth: "40%", |
| | | label: { |
| | | show: true, |
| | | position: "top", |
| | | color: "#5470c6", |
| | | fontSize: 10, |
| | | formatter: (params) => params.value |
| | | } |
| | | }, |
| | | { |
| | | name: "出库", |
| | | type: "line", |
| | | data: outboundData, |
| | | symbol: "circle", |
| | | symbolSize: 6, |
| | | itemStyle: { color: "#91cc75" }, |
| | | lineStyle: { width: 2, type: "solid" }, |
| | | smooth: false, |
| | | label: { |
| | | show: true, |
| | | position: "top", |
| | | color: "#91cc75", |
| | | fontSize: 10, |
| | | formatter: (params) => params.value |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | this.charts.stockAge.setOption(option, true); |
| | | chart.setOption(option, true); |
| | | }, |
| | | |
| | | updateWarehouseChart() { |
| | | if (!this.charts.warehouse) return; |
| | | |
| | | if (!this.warehouseData.length) { |
| | | this.charts.warehouse.setOption({ |
| | | title: { |
| | | show: true, |
| | | text: '暂无仓库数据', |
| | | left: 'center', |
| | | top: 'center', |
| | | textStyle: { color: '#ccc' } |
| | | } |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const warehouseNames = this.warehouseData.map(w => w.warehouse); |
| | | const hasStocks = this.warehouseData.map(w => w.hasStock); |
| | | const noStocks = this.warehouseData.map(w => w.noStock); |
| | | const hasStockPercentages = this.warehouseData.map(w => w.hasStockPercentage); |
| | | const noStockPercentages = this.warehouseData.map(w => w.noStockPercentage); |
| | | |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { type: "shadow" }, |
| | | formatter: (params) => { |
| | | let tip = params[0].name + "<br/>"; |
| | | params.forEach(param => { |
| | | const dataIndex = param.dataIndex; |
| | | const warehouse = this.warehouseData[dataIndex]; |
| | | if (warehouse) { |
| | | if (param.seriesName === "已用容量") { |
| | | tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.hasStockPercentage})<br/>`; |
| | | tip += `有库存: ${warehouse.hasStock}<br/>`; |
| | | tip += `无库存: ${warehouse.noStock}<br/>`; |
| | | tip += `总容量: ${warehouse.total}`; |
| | | } else if (param.seriesName === "剩余容量") { |
| | | tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.noStockPercentage})<br/>`; |
| | | } |
| | | } else { |
| | | tip += `${param.marker}${param.seriesName}: ${param.value}<br/>`; |
| | | } |
| | | }); |
| | | return tip; |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ["已用容量", "剩余容量"], |
| | | textStyle: { color: "#fff" } |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.warehouseData.map(w => w.Warehouse), |
| | | axisLabel: { color: "#fff", rotate: 30 } |
| | | data: warehouseNames, |
| | | axisLabel: { color: "#fff", rotate: 30, interval: 0 } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "容量", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "已用容量", |
| | | type: "bar", |
| | | data: this.warehouseData.map(w => w.Count), |
| | | itemStyle: { color: "#5470c6" } |
| | | data: hasStocks.map((value, index) => ({ |
| | | value: value, |
| | | label: { |
| | | show: true, |
| | | position: "top", |
| | | formatter: () => { |
| | | const pct = hasStockPercentages[index]; |
| | | return `${value} (${pct})`; |
| | | }, |
| | | color: "#91cc75", |
| | | fontSize: 11 |
| | | } |
| | | })), |
| | | itemStyle: { color: "#91cc75" } |
| | | }, |
| | | { |
| | | name: "剩余容量", |
| | | type: "bar", |
| | | data: noStocks.map((value, index) => ({ |
| | | value: value, |
| | | label: { |
| | | show: true, |
| | | position: "top", |
| | | formatter: () => { |
| | | const pct = noStockPercentages[index]; |
| | | return `${value} (${pct})`; |
| | | }, |
| | | color: "#fac858", |
| | | fontSize: 11 |
| | | } |
| | | })), |
| | | itemStyle: { color: "#fac858" } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | this.charts.warehouse.setOption(option, true); |
| | | } |
| | | } |
| | |
| | | <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 { |
| | | /* KPI 卡片样式 - 5列布局 */ |
| | | .kpi-cards { |
| | | display: grid; |
| | | grid-template-columns: repeat(5, 1fr); |
| | | gap: 20px; |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .kpi-card { |
| | | background: rgba(10, 16, 35, 0.7); |
| | | backdrop-filter: blur(10px); |
| | | border: 1px solid rgba(64, 224, 208, 0.3); |
| | | border-radius: 16px; |
| | | padding: 16px 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | transition: all 0.3s ease; |
| | | box-shadow: 0 0 15px rgba(0, 255, 255, 0.1); |
| | | } |
| | | |
| | | .kpi-card:hover { |
| | | transform: translateY(-3px); |
| | | border-color: rgba(64, 224, 208, 0.6); |
| | | box-shadow: 0 0 25px rgba(0, 255, 255, 0.2); |
| | | } |
| | | |
| | | .kpi-icon { |
| | | font-size: 32px; |
| | | opacity: 0.9; |
| | | } |
| | | |
| | | .kpi-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .kpi-label { |
| | | font-size: 13px; |
| | | color: #8ba0b5; |
| | | margin-bottom: 6px; |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .kpi-value { |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | color: #00ffff; |
| | | text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | /* 每日图表布局 - 每行2个 */ |
| | | .chart-row.daily-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, 1fr); |
| | | 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; |
| | | 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; |
| | | font-size: 16px; |
| | | color: #00ffff; |
| | | font-size: 15px; |
| | | text-align: center; |
| | | margin-bottom: 10px; |
| | | margin-bottom: 12px; |
| | | text-shadow: 0 0 10px rgba(0, 255, 255, 0.7); |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | /* 仓库数字显示区域 */ |
| | | .warehouse-numbers { |
| | | display: flex; |
| | | justify-content: space-around; |
| | | margin-bottom: 12px; |
| | | padding: 8px 0; |
| | | background: rgba(0, 0, 0, 0.3); |
| | | border-radius: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .number-item { |
| | | text-align: center; |
| | | flex: 1; |
| | | min-width: 80px; |
| | | } |
| | | |
| | | .number-label { |
| | | display: block; |
| | | font-size: 11px; |
| | | color: #8ba0b5; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .number-value { |
| | | display: block; |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .number-item.inbound .number-value { |
| | | color: #5470c6; |
| | | } |
| | | |
| | | .number-item.battery .number-value { |
| | | color: #ee6666; |
| | | } |
| | | |
| | | .number-item.empty-tray .number-value { |
| | | color: #fc8452; |
| | | } |
| | | |
| | | .chart-content { |
| | |
| | | width: 100%; |
| | | } |
| | | |
| | | .stock-total { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 280px; |
| | | @media (max-width: 768px) { |
| | | .kpi-cards { |
| | | grid-template-columns: repeat(2, 1fr); |
| | | } |
| | | .chart-row.daily-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | .chart-content { |
| | | height: 240px; |
| | | } |
| | | .card-title { |
| | | font-size: 13px; |
| | | white-space: normal; |
| | | } |
| | | .number-value { |
| | | font-size: 14px; |
| | | } |
| | | .number-item { |
| | | min-width: 60px; |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | .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; |
| | | } |
| | | |
| | | .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> |
| | | </style> |