| | |
| | | <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">{{ totalWarehouses }}</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">{{ totalStock.toLocaleString() }}</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">{{ monthlyInboundTotal.toLocaleString() }}</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> |
| | | <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">{{ monthlyOutboundTotal.toLocaleString() }}</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> |
| | | <!-- 顶部:本月出入库趋势 - 上3下2布局,每个卡片直接显示仓库数字 --> |
| | | <div class="chart-row top-three"> |
| | | <div v-for="warehouse in topWarehouses" :key="warehouse.code" class="chart-card"> |
| | | <div class="card-title">{{ warehouse.name }}</div> |
| | | <!-- 仓库数字显示区域 --> |
| | | <div class="warehouse-numbers"> |
| | | <div class="number-item inbound"> |
| | | <span class="number-label">入库</span> |
| | | <span class="number-value">{{ getMonthlyInbound(warehouse.code) }}</span> |
| | | </div> |
| | | <div class="number-item outbound"> |
| | | <span class="number-label">出库</span> |
| | | <span class="number-value">{{ getMonthlyOutbound(warehouse.code) }}</span> |
| | | </div> |
| | | <div class="number-item stock"> |
| | | <span class="number-label">库存</span> |
| | | <span class="number-value">{{ getWarehouseStock(warehouse.code) }}</span> |
| | | </div> |
| | | </div> |
| | | <div :id="`chart-${warehouse.code}`" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-row bottom-two"> |
| | | <div v-for="warehouse in bottomWarehouses" :key="warehouse.code" class="chart-card"> |
| | | <div class="card-title">{{ warehouse.name }}</div> |
| | | <!-- 仓库数字显示区域 --> |
| | | <div class="warehouse-numbers"> |
| | | <div class="number-item inbound"> |
| | | <span class="number-label">入库</span> |
| | | <span class="number-value">{{ getMonthlyInbound(warehouse.code) }}</span> |
| | | </div> |
| | | <div class="number-item outbound"> |
| | | <span class="number-label">出库</span> |
| | | <span class="number-value">{{ getMonthlyOutbound(warehouse.code) }}</span> |
| | | </div> |
| | | <div class="number-item stock"> |
| | | <span class="number-label">库存</span> |
| | | <span class="number-value">{{ getWarehouseStock(warehouse.code) }}</span> |
| | | </div> |
| | | </div> |
| | | <div :id="`chart-${warehouse.code}`" class="chart-content"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 每日出入库趋势 (全宽) --> |
| | | <div class="chart-row full-width"> |
| | | <div class="chart-card"> |
| | | <div class="card-title">每日出入库趋势</div> |
| | | <div id="chart-daily" 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 |
| | | // 五个仓库定义 - 上3个 |
| | | topWarehouses: [ |
| | | { code: "GWSC1", name: "高温1号仓库" }, |
| | | { code: "CWSC1", name: "常温1号仓库" }, |
| | | { code: "HCSC1", name: "分容1号仓库" } |
| | | ], |
| | | // 下2个 |
| | | bottomWarehouses: [ |
| | | { code: "FJSC1", name: "负极卷1号仓库" }, |
| | | { code: "ZJSC1", name: "正极卷1号仓库" } |
| | | ], |
| | | dailyData: [], |
| | | // 存储每个仓库的月度数据 |
| | | monthlyData: { |
| | | GWSC1: [], |
| | | CWSC1: [], |
| | | HCSC1: [], |
| | | FJSC1: [], |
| | | ZJSC1: [] |
| | | }, |
| | | weeklyData: [], |
| | | monthlyData: [], |
| | | stockAgeData: [], |
| | | warehouseData: [] |
| | | // 存储每个仓库的当前库存 |
| | | warehouseStocks: { |
| | | GWSC1: 0, |
| | | CWSC1: 0, |
| | | HCSC1: 0, |
| | | FJSC1: 0, |
| | | ZJSC1: 0 |
| | | }, |
| | | warehouseData: [], |
| | | // KPI 汇总数据 |
| | | totalWarehouses: 5, |
| | | totalStock: 0, |
| | | monthlyInboundTotal: 0, |
| | | monthlyOutboundTotal: 0 |
| | | }; |
| | | }, |
| | | mounted() { |
| | |
| | | }, |
| | | 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")); |
| | | // 初始化所有仓库图表 |
| | | const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses]; |
| | | allWarehouses.forEach(warehouse => { |
| | | const chartId = `chart-${warehouse.code}`; |
| | | const el = document.getElementById(chartId); |
| | | if (el) { |
| | | this.charts[warehouse.code] = echarts.init(el); |
| | | } |
| | | }); |
| | | // 初始化每日图表和仓库分布图表 |
| | | this.charts.daily = echarts.init(document.getElementById("chart-daily")); |
| | | this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse")); |
| | | }, |
| | | |
| | | async loadData() { |
| | | await this.loadOverview(); |
| | | await this.loadWeeklyStats(); |
| | | await this.loadMonthlyStats(); |
| | | await this.loadStockAgeDistribution(); |
| | | // 并行加载所有仓库的月度数据(分别传入不同的Roadway参数) |
| | | const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses]; |
| | | const monthlyPromises = allWarehouses.map(warehouse => |
| | | this.loadMonthlyStatsForWarehouse(warehouse.code) |
| | | ); |
| | | await Promise.all(monthlyPromises); |
| | | // 更新所有仓库的月度图表 |
| | | this.updateAllMonthlyTrendCharts(); |
| | | |
| | | await this.loadDailyStats(); |
| | | await this.loadStockByWarehouse(); |
| | | await this.loadWarehouseStocks(); |
| | | this.calculateKPIs(); |
| | | }, |
| | | |
| | | async loadOverview() { |
| | | async loadMonthlyStatsForWarehouse(roadway) { |
| | | console.log(`正在加载${roadway}的每月统计数据...`); |
| | | 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(); |
| | | // 关键修复:分别传入不同的Roadway参数 |
| | | const res = await this.http.get("/api/Dashboard/MonthlyStats?monthly=12&roadway=" + roadway); |
| | | if (res.status && res.data) { |
| | | console.log(`${roadway} 每月统计数据:`, res.data); |
| | | this.monthlyData[roadway] = res.data; |
| | | } else { |
| | | this.monthlyData[roadway] = []; |
| | | } |
| | | } catch (e) { |
| | | console.error("加载总览数据失败", e); |
| | | console.error(`加载${roadway}每月统计失败`, e); |
| | | this.monthlyData[roadway] = []; |
| | | } |
| | | }, |
| | | |
| | | async loadWeeklyStats() { |
| | | async loadDailyStats() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 }); |
| | | if (res.Status && res.Data) { |
| | | this.weeklyData = res.Data; |
| | | this.updateWeekChart(); |
| | | const res = await this.http.get("/api/Dashboard/DailyStats", { days: 30 }); |
| | | if (res.status && res.data) { |
| | | console.log("每日统计数据:", res.data); |
| | | this.dailyData = res.data; |
| | | this.updateDailyChart(); |
| | | } |
| | | } 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); |
| | | console.error("加载每日统计失败", e); |
| | | } |
| | | }, |
| | | |
| | | async loadStockByWarehouse() { |
| | | try { |
| | | const res = await this.http.get("/api/Dashboard/StockByWarehouse"); |
| | | if (res.Status && res.Data) { |
| | | this.warehouseData = res.Data; |
| | | if (res.status && res.data) { |
| | | console.log("仓库分布数据:", res.data); |
| | | this.warehouseData = res.data.data || res.data; |
| | | this.updateWarehouseChart(); |
| | | } |
| | | } catch (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" } |
| | | async loadWarehouseStocks() { |
| | | // 模拟加载每个仓库的当前库存量 |
| | | // 如果后端有接口,可以替换为真实API调用 |
| | | try { |
| | | // 尝试加载库存数据,如果接口不存在则使用模拟数据 |
| | | const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses]; |
| | | for (const warehouse of allWarehouses) { |
| | | try { |
| | | const res = await this.http.get(`/api/Dashboard/WarehouseStock?warehouse=${warehouse.code}`); |
| | | if (res.status && res.data) { |
| | | this.warehouseStocks[warehouse.code] = res.data.stock || 0; |
| | | } else { |
| | | // 从月度数据中计算模拟库存(最近月份累计入库-出库) |
| | | const monthlyData = this.monthlyData[warehouse.code] || []; |
| | | let totalInbound = 0; |
| | | let totalOutbound = 0; |
| | | monthlyData.forEach(m => { |
| | | totalInbound += (m.inbound ?? m.Inbound) || 0; |
| | | totalOutbound += (m.outbound ?? m.Outbound) || 0; |
| | | }); |
| | | this.warehouseStocks[warehouse.code] = Math.max(0, totalInbound - totalOutbound); |
| | | } |
| | | } catch (e) { |
| | | // 使用模拟数据 |
| | | const mockStocks = { |
| | | GWSC1: 12580, |
| | | CWSC1: 8920, |
| | | HCSC1: 15600, |
| | | FJSC1: 4300, |
| | | ZJSC1: 7200 |
| | | }; |
| | | this.warehouseStocks[warehouse.code] = mockStocks[warehouse.code] || 0; |
| | | } |
| | | ], |
| | | 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); |
| | | } |
| | | } catch (e) { |
| | | console.error("加载仓库库存失败", e); |
| | | } |
| | | }, |
| | | |
| | | updateStockAgeChart() { |
| | | getMonthlyInbound(warehouseCode) { |
| | | const data = this.monthlyData[warehouseCode] || []; |
| | | if (data.length === 0) return 0; |
| | | // 获取最近一个月(最后一条)的入库数 |
| | | const latest = data[data.length - 1]; |
| | | return (latest.inbound ?? latest.Inbound) || 0; |
| | | }, |
| | | |
| | | getMonthlyOutbound(warehouseCode) { |
| | | const data = this.monthlyData[warehouseCode] || []; |
| | | if (data.length === 0) return 0; |
| | | // 获取最近一个月(最后一条)的出库数 |
| | | const latest = data[data.length - 1]; |
| | | return (latest.outbound ?? latest.Outbound) || 0; |
| | | }, |
| | | |
| | | getWarehouseStock(warehouseCode) { |
| | | return this.warehouseStocks[warehouseCode] || 0; |
| | | }, |
| | | |
| | | calculateKPIs() { |
| | | // 计算总库存 |
| | | let totalStock = 0; |
| | | for (const code in this.warehouseStocks) { |
| | | totalStock += this.warehouseStocks[code]; |
| | | } |
| | | this.totalStock = totalStock; |
| | | |
| | | // 计算本月总入库和总出库(所有仓库最近一个月的合计) |
| | | let totalInbound = 0; |
| | | let totalOutbound = 0; |
| | | const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses]; |
| | | allWarehouses.forEach(warehouse => { |
| | | totalInbound += this.getMonthlyInbound(warehouse.code); |
| | | totalOutbound += this.getMonthlyOutbound(warehouse.code); |
| | | }); |
| | | this.monthlyInboundTotal = totalInbound; |
| | | this.monthlyOutboundTotal = totalOutbound; |
| | | }, |
| | | |
| | | // 更新所有仓库的月度趋势图表 |
| | | updateAllMonthlyTrendCharts() { |
| | | const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses]; |
| | | allWarehouses.forEach(warehouse => { |
| | | this.updateMonthlyTrendChartForWarehouse(warehouse.code); |
| | | }); |
| | | }, |
| | | |
| | | updateMonthlyTrendChartForWarehouse(roadway) { |
| | | const chart = this.charts[roadway]; |
| | | if (!chart) return; |
| | | |
| | | const data = this.monthlyData[roadway] || []; |
| | | // 兼容大小写字段名 |
| | | const monthLabels = data.map(m => m.month || m.Month || ""); |
| | | const inboundData = data.map(m => { |
| | | const val = m.inbound ?? m.Inbound; |
| | | return val !== undefined && val !== null ? Number(val) : 0; |
| | | }); |
| | | const outboundData = data.map(m => { |
| | | const val = m.outbound ?? m.Outbound; |
| | | return val !== undefined && val !== null ? Number(val) : 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: monthLabels, |
| | | 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: "35%", |
| | | label: { |
| | | show: inboundData.length <= 8, |
| | | position: "top", |
| | | color: "#5470c6", |
| | | fontSize: 10 |
| | | } |
| | | }, |
| | | { |
| | | name: "出库", |
| | | type: "line", |
| | | data: outboundData, |
| | | symbol: "circle", |
| | | symbolSize: 6, |
| | | itemStyle: { color: "#91cc75" }, |
| | | lineStyle: { width: 2, type: "solid" }, |
| | | smooth: false, |
| | | label: { |
| | | show: outboundData.length <= 8, |
| | | position: "top", |
| | | color: "#91cc75", |
| | | fontSize: 10 |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | this.charts.stockAge.setOption(option, true); |
| | | chart.setOption(option, true); |
| | | }, |
| | | |
| | | updateDailyChart() { |
| | | if (!this.charts.daily) return; |
| | | const option = { |
| | | tooltip: { trigger: "axis" }, |
| | | legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: this.dailyData.map(d => d.date), |
| | | axisLabel: { |
| | | color: "#fff", |
| | | interval: 0, |
| | | rotate: 45, |
| | | fontSize: 12, |
| | | margin: 10 |
| | | }, |
| | | axisTick: { |
| | | alignWithLabel: true |
| | | } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | axisLabel: { color: "#fff" } |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "15%", |
| | | top: "10%", |
| | | containLabel: true |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "入库", |
| | | type: "bar", |
| | | data: this.dailyData.map(d => d.inbound), |
| | | itemStyle: { color: "#5470c6" } |
| | | }, |
| | | { |
| | | name: "出库", |
| | | type: "bar", |
| | | data: this.dailyData.map(d => d.outbound), |
| | | itemStyle: { color: "#91cc75" } |
| | | } |
| | | ] |
| | | }; |
| | | this.charts.daily.setOption(option, true); |
| | | }, |
| | | |
| | | updateWarehouseChart() { |
| | | if (!this.charts.warehouse) 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: function(params) { |
| | | let tip = params[0].name + "<br/>"; |
| | | params.forEach(param => { |
| | | const dataIndex = param.dataIndex; |
| | | const warehouse = window.homeComponent?.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: (params) => { |
| | | const pct = hasStockPercentages[params.dataIndex]; |
| | | return `${params.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: (params) => { |
| | | const pct = noStockPercentages[params.dataIndex]; |
| | | return `${params.value} (${pct})`; |
| | | }, |
| | | color: "#fac858", |
| | | fontSize: 11 |
| | | } |
| | | })), |
| | | itemStyle: { color: "#fac858" } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | window.homeComponent = this; |
| | | 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 卡片样式 */ |
| | | .kpi-cards { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, 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; |
| | | } |
| | | |
| | | /* 上3个图表布局 */ |
| | | .chart-row.top-three { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, 1fr); |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* 下2个图表布局 */ |
| | | .chart-row.bottom-two { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, 1fr); |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .chart-row.full-width { |
| | | width: 100%; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .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); |
| | | } |
| | | |
| | | .chart-card::before, |
| | | .chart-card::after { |
| | | animation: neon-flicker 2s infinite alternate; |
| | | } |
| | | |
| | | @keyframes neon-flicker { |
| | | 0%, |
| | | 100% { |
| | | opacity: 1; |
| | | box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7); |
| | | } |
| | | 50% { |
| | | opacity: 0.8; |
| | | box-shadow: -2px -2px 5px #00ffff, 0 0 5px rgba(0, 255, 255, 0.5); |
| | | } |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | |
| | | .number-item { |
| | | text-align: center; |
| | | flex: 1; |
| | | } |
| | | |
| | | .number-label { |
| | | display: block; |
| | | font-size: 11px; |
| | | color: #8ba0b5; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .number-value { |
| | | display: block; |
| | | font-size: 20px; |
| | | font-weight: 700; |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .number-item.inbound .number-value { |
| | | color: #5470c6; |
| | | } |
| | | |
| | | .number-item.outbound .number-value { |
| | | color: #91cc75; |
| | | } |
| | | |
| | | .number-item.stock .number-value { |
| | | color: #fac858; |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 280px; |
| | | height: 240px; |
| | | 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> |
| | | |
| | | /* 响应式调整 */ |
| | | @media (max-width: 1024px) { |
| | | .kpi-cards { |
| | | grid-template-columns: repeat(2, 1fr); |
| | | } |
| | | .chart-row.top-three { |
| | | grid-template-columns: repeat(2, 1fr); |
| | | } |
| | | .chart-row.bottom-two { |
| | | grid-template-columns: repeat(2, 1fr); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .kpi-cards { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | .chart-row.top-three { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | .chart-row.bottom-two { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | .chart-content { |
| | | height: 220px; |
| | | } |
| | | .full-width .chart-content { |
| | | height: 280px; |
| | | } |
| | | .card-title { |
| | | font-size: 13px; |
| | | white-space: normal; |
| | | } |
| | | .number-value { |
| | | font-size: 16px; |
| | | } |
| | | } |
| | | |
| | | /* 添加网格线效果 */ |
| | | .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> |