xiazhengtongxue
3 天以前 51922d7093b9c8f52417bfdd0fe9aa087d1fb5be
feat: 优化仓库仪表盘界面并添加电池和空托盘统计功能

refactor(robotTask): 添加机器人任务异常信息和备注字段
fix(appsettings): 禁用自动出库任务功能
refactor(RobotPrefixCommandHandler): 统一使用BuildStockDTO方法
feat(stockInfo): 添加货位id字段显示
refactor(Dt_RobotTask): 设置部分字段为可空
feat(DashboardController): 添加电池和空托盘数量统计接口
refactor(Home.vue): 重构仪表盘界面布局和数据显示
已修改8个文件
853 ■■■■ 文件已修改
Code/WCS/WIDESEAWCS_Client/src/views/taskinfo/robotTask.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/TaskInfo/Dt_RobotTask.cs 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue 695 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockInfo.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Client/src/views/taskinfo/robotTask.vue
@@ -42,6 +42,9 @@
      robotSourceAddressPalletCode: "",
      robotTargetAddressPalletCode: "",
      robotGrade: 2,
      robotExceptionMessage: "",
      robotDispatchertime: "",
      robotRemark: "",
    });
    // 编辑表单配置
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/TaskInfo/Dt_RobotTask.cs
@@ -66,31 +66,31 @@
        /// <summary>
        /// 机器人来源地址输送线编号
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址输送线编号")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址输送线编号", IsNullable = true)]
        public string RobotSourceAddressLineCode { get; set; }
        /// <summary>
        /// 机器人目标地址输送线编号
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址输送线编号")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址输送线编号", IsNullable = true)]
        public string RobotTargetAddressLineCode { get; set; }
        /// <summary>
        /// 机器人来源地址输送线托盘号
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址输送线托盘号")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人来源地址输送线托盘号", IsNullable = true)]
        public string RobotSourceAddressPalletCode { get; set; }
        /// <summary>
        /// 机器人目标地址线托盘号
        /// </summary>
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址线托盘号")]
        [SugarColumn(Length = 20, ColumnDescription = "机器人目标地址线托盘号", IsNullable = true)]
        public string RobotTargetAddressPalletCode { get; set; }
        /// <summary>
        /// 机器人异常信息
        /// </summary>
        [SugarColumn(ColumnDescription = "机器人异常信息")]
        [SugarColumn(ColumnDescription = "机器人异常信息", IsNullable = true)]
        public string RobotExceptionMessage { get; set; }
        /// <summary>
@@ -102,13 +102,13 @@
        /// <summary>
        /// 机器人调度时间
        /// </summary>
        [SugarColumn(ColumnDescription = "机器人调度时间")]
        [SugarColumn(ColumnDescription = "机器人调度时间", IsNullable = true)]
        public DateTime? RobotDispatchertime { get; set; }
        /// <summary>
        /// 机器人备注
        /// </summary>
        [SugarColumn(Length = 50, ColumnDescription = "机器人备注")]
        [SugarColumn(Length = 50, ColumnDescription = "机器人备注", IsNullable = true)]
        public string RobotRemark { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
@@ -714,56 +714,7 @@
                    .ToList()
            };
        }
        /// <summary>
        /// 构建库存回传 DTO
        /// </summary>
        /// <remarks>
        /// 用于拆盘和组盘操作时,向 WMS 回传库存信息。
        /// DTO 包含源货位、目标货位、托盘码以及每个位置的电池条码。
        /// </remarks>
        /// <param name="state">机器人当前状态</param>
        /// <param name="positions">电池位置数组</param>
        /// <returns>构建好的库存 DTO</returns>
        public static StockDTO BuildStockDTO1(RobotSocketState state, int[] positions)
        {
            return new StockDTO
            {
                // 源输送线编号
                SourceLineNo = state.CurrentTask.RobotSourceAddressLineCode,
                // 源托盘号
                SourcePalletNo = state.CurrentTask.RobotSourceAddressPalletCode,
                // 目标托盘号
                TargetPalletNo = state.CurrentTask.RobotTargetAddressPalletCode,
                // 目标输送线编号
                TargetLineNo = state.CurrentTask.RobotTargetAddressLineCode,
                // 巷道编号(机器人名称)
                Roadway = state.CurrentTask.RobotRoadway,
                // 电池位置详情列表
                // 过滤掉位置为 0 或负数的无效数据
                // 按位置编号排序
                // 为每个位置生成对应的库存详情
                Details = positions
                    .Where(x => x > 0)  // 过滤无效位置
                    .OrderBy(x => x)   // 按位置排序
                    .Select((x, idx) => new StockDetailDTO
                    {
                        // 数量:如果已有任务总数,使用任务总数+当前位置数;否则只使用当前位置数
                        Quantity = 1,
                        // 通道/位置编号
                        Channel = x,
                        // 电池条码:取当前批次条码列表的最后 N 个(N = 有效位置数)
                        CellBarcode = !state.CurrentBatchBarcodes.IsNullOrEmpty() ? state.CurrentBatchBarcodes[^(positions.Count(p => p > 0) - idx)].ToString() ?? string.Empty : string.Empty
                    })
                    .ToList()
            };
        }
        /// <summary>
        /// 调用拆盘 API
        /// </summary>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotPrefixCommandHandler.cs
@@ -326,7 +326,7 @@
                else
                {
                    // 组盘任务:原有逻辑
                    var stockDTO = RobotTaskProcessor.BuildStockDTO1(state, positions);
                    var stockDTO = RobotTaskProcessor.BuildStockDTO(state, positions);
                    var result = _taskProcessor.PostGroupPalletAsync(nameof(ConfigKey.GroupPalletAsync), stockDTO);
                    putSuccess = result.Data.Status && result.IsSuccess;
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue
@@ -1,91 +1,78 @@
<template>
  <div class="dashboard-container">
    <!-- 顶部KPI卡片:显示仓库总数和总库存量 -->
    <!-- 顶部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 class="kpi-label">总货位</div>
          <div class="kpi-value">{{ totalLocation }}</div>
        </div>
      </div>
      <div class="kpi-card">
        <div class="kpi-icon">📦</div>
        <div class="kpi-icon">🔥</div>
        <div class="kpi-info">
          <div class="kpi-label">总库存量</div>
          <div class="kpi-value">{{ totalStock.toLocaleString() }}</div>
          <div class="kpi-label">化成库</div>
          <div class="kpi-value">{{ warehouseLocations.hc }}</div>
        </div>
      </div>
      <div class="kpi-card">
        <div class="kpi-icon">📊</div>
        <div class="kpi-icon">🌡️</div>
        <div class="kpi-info">
          <div class="kpi-label">本月总入库</div>
          <div class="kpi-value">{{ monthlyInboundTotal.toLocaleString() }}</div>
          <div class="kpi-label">高温库</div>
          <div class="kpi-value">{{ warehouseLocations.gw }}</div>
        </div>
      </div>
      <div class="kpi-card">
        <div class="kpi-icon">📤</div>
        <div class="kpi-icon">❄️</div>
        <div class="kpi-info">
          <div class="kpi-label">本月总出库</div>
          <div class="kpi-value">{{ monthlyOutboundTotal.toLocaleString() }}</div>
          <div class="kpi-label">常温库</div>
          <div class="kpi-value">{{ warehouseLocations.cw }}</div>
        </div>
      </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>
    <!-- 顶部:本月出入库趋势 - 上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>
        <!-- 仓库数字显示区域 -->
    <!-- 第一行: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">{{ getMonthlyInbound(warehouse.code) }}</span>
              <span class="number-label">电池数量</span>
              <span class="number-value">{{ getBatteryCount(warehouse.code) }}</span>
          </div>
          <div class="number-item outbound">
            <span class="number-label">出库</span>
            <span class="number-value">{{ getMonthlyOutbound(warehouse.code) }}</span>
            <div class="number-item empty-tray">
              <span class="number-label">空托盘数量</span>
              <span class="number-value">{{ getEmptyTrayCount(warehouse.code) }}</span>
          </div>
          <div class="number-item stock">
            <span class="number-label">库存</span>
            <span class="number-value">{{ getWarehouseStock(warehouse.code) }}</span>
          </template>
          </div>
        </div>
        <div :id="`chart-${warehouse.code}`" class="chart-content"></div>
        <div :id="`daily-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>
@@ -103,41 +90,54 @@
  data() {
    return {
      charts: {},
      // 五个仓库定义 - 上3个
      topWarehouses: [
        { code: "GWSC1", name: "高温1号仓库" },
        { code: "CWSC1", name: "常温1号仓库" },
        { code: "HCSC1", name: "分容1号仓库" }
      // 四个核心仓库(合并了正负极卷库为极卷库)
      dailyWarehouses: [
        { code: "HCSC1", name: "化成库", type: "hc" },
        { code: "GWSC1", name: "高温库", type: "gw" },
        { code: "CWSC1", name: "常温库", type: "cw" },
        { code: "ROLL", name: "极卷库", type: "jj" }
      ],
      // 下2个
      bottomWarehouses: [
        { code: "FJSC1", name: "负极卷1号仓库" },
        { code: "ZJSC1", name: "正极卷1号仓库" }
      ],
      dailyData: [],
      // 存储每个仓库的月度数据
      monthlyData: {
      // 原始每日数据存储 (其中ROLL为合并后的极卷库数据)
      dailyDataMap: {
        GWSC1: [],
        CWSC1: [],
        HCSC1: [],
        FJSC1: [],
        ZJSC1: []
        ROLL: []
      },
      // 存储每个仓库的当前库存
      // 存储每个仓库的电池数量
      warehouseStocks: {
        GWSC1: 0,
        CWSC1: 0,
        HCSC1: 0,
        FJSC1: 0,
        ZJSC1: 0
        ROLL: 0
      },
      warehouseData: [],
      // KPI 汇总数据
      totalWarehouses: 5,
      totalStock: 0,
      monthlyInboundTotal: 0,
      monthlyOutboundTotal: 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();
@@ -154,181 +154,265 @@
    },
    initCharts() {
      // 初始化所有仓库图表
      const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses];
      allWarehouses.forEach(warehouse => {
        const chartId = `chart-${warehouse.code}`;
      // 初始化每日图表
      this.dailyWarehouses.forEach(warehouse => {
        const chartId = `daily-chart-${warehouse.code}`;
        const el = document.getElementById(chartId);
        if (el) {
          this.charts[warehouse.code] = echarts.init(el);
          this.charts[`daily-${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() {
      // 并行加载所有仓库的月度数据(分别传入不同的Roadway参数)
      const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses];
      const monthlyPromises = allWarehouses.map(warehouse =>
        this.loadMonthlyStatsForWarehouse(warehouse.code)
      );
      await Promise.all(monthlyPromises);
      // 更新所有仓库的月度图表
      this.updateAllMonthlyTrendCharts();
      try {
        // 并行加载所有数据
        await Promise.all([
          this.loadAllDailyStats(),
          this.loadStockAndTrayCount(),
          this.loadStockByWarehouse()
        ]);
      await this.loadDailyStats();
      await this.loadStockByWarehouse();
      await this.loadWarehouseStocks();
      this.calculateKPIs();
        // 更新所有图表
        this.updateAllDailyCharts();
      } catch (error) {
        console.error("加载数据失败:", error);
        this.$message?.error("数据加载失败,请稍后重试");
      }
    },
    async loadMonthlyStatsForWarehouse(roadway) {
      console.log(`正在加载${roadway}的每月统计数据...`);
      try {
        // 关键修复:分别传入不同的Roadway参数
        const res = await this.http.get("/api/Dashboard/MonthlyStats?monthly=12&roadway=" + roadway);
    async loadAllDailyStats() {
      console.log("正在加载所有仓库的每日统计数据...");
      const res = await this.http.get("/api/Dashboard/DailyStats?days=10");
        if (res.status && res.data) {
          console.log(`${roadway} 每月统计数据:`, res.data);
          this.monthlyData[roadway] = 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 {
          this.monthlyData[roadway] = [];
        }
      } catch (e) {
        console.error(`加载${roadway}每月统计失败`, e);
        this.monthlyData[roadway] = [];
        console.error("获取每日数据失败");
      }
    },
    async loadDailyStats() {
      try {
        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();
    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 });
        }
      } catch (e) {
        console.error("加载每日统计失败", e);
            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 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 {
      console.log("正在加载仓库分布数据...");
        const res = await this.http.get("/api/Dashboard/StockByWarehouse");
        if (res.status && res.data) {
          console.log("仓库分布数据:", res.data);
          this.warehouseData = res.data.data || res.data;
          this.updateWarehouseChart();
        }
      } catch (e) {
        console.error("加载仓库分布失败", e);
      }
    },
        const rawData = res.data.data || res.data;
    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;
        // 处理极卷库合并
        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 {
              // 从月度数据中计算模拟库存(最近月份累计入库-出库)
              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;
            otherWarehouses.push(item);
          }
              });
              this.warehouseStocks[warehouse.code] = Math.max(0, totalInbound - totalOutbound);
        // 添加合并后的极卷库
        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) {
            // 使用模拟数据
            const mockStocks = {
              GWSC1: 12580,
              CWSC1: 8920,
              HCSC1: 15600,
              FJSC1: 4300,
              ZJSC1: 7200
            };
            this.warehouseStocks[warehouse.code] = mockStocks[warehouse.code] || 0;
          }
        }
      } catch (e) {
        console.error("加载仓库库存失败", e);
        this.warehouseData = otherWarehouses;
        this.updateWarehouseChart();
      } else {
        console.error("获取仓库分布数据失败");
        throw new Error("获取仓库分布数据失败");
      }
    },
    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;
    getDailyTotalInbound(warehouseCode) {
      const data = this.dailyDataMap[warehouseCode] || [];
      return data.reduce((sum, item) => sum + (item.inbound || 0), 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;
    getDailyTotalOutbound(warehouseCode) {
      const data = this.dailyDataMap[warehouseCode] || [];
      return data.reduce((sum, item) => sum + (item.outbound || 0), 0);
    },
    getWarehouseStock(warehouseCode) {
      return this.warehouseStocks[warehouseCode] || 0;
    },
    calculateKPIs() {
      // 计算总库存
      let totalStock = 0;
      for (const code in this.warehouseStocks) {
        totalStock += this.warehouseStocks[code];
    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;
      }
      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;
      return 0;
    },
    // 更新所有仓库的月度趋势图表
    updateAllMonthlyTrendCharts() {
      const allWarehouses = [...this.topWarehouses, ...this.bottomWarehouses];
      allWarehouses.forEach(warehouse => {
        this.updateMonthlyTrendChartForWarehouse(warehouse.code);
    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;
    },
    updateAllDailyCharts() {
      this.dailyWarehouses.forEach(warehouse => {
        this.updateDailyChartForWarehouse(warehouse.code);
      });
    },
    updateMonthlyTrendChartForWarehouse(roadway) {
      const chart = this.charts[roadway];
    updateDailyChartForWarehouse(roadway) {
      const chart = this.charts[`daily-${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 data = this.dailyDataMap[roadway] || [];
      // 如果没有数据,显示空图表提示
      if (!data.length) {
        chart.setOption({
          title: {
            show: true,
            text: '暂无数据',
            left: 'center',
            top: 'center',
            textStyle: { color: '#ccc', fontSize: 14 }
          }
        }, true);
        return;
      }
      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: { 
@@ -358,7 +442,7 @@
        },
        xAxis: {
          type: "category",
          data: monthLabels,
          data: dates,
          axisLabel: {
            color: "#ccc",
            rotate: 45,
@@ -370,7 +454,7 @@
        },
        yAxis: {
          type: "value",
          name: "任务数量",
          name: "数量",
          nameTextStyle: { color: "#ccc", fontSize: 11 },
          axisLabel: { color: "#ccc" },
          splitLine: { lineStyle: { color: "#2a3a4a", type: "dashed" } }
@@ -384,12 +468,13 @@
              color: "#5470c6",
              borderRadius: [4, 4, 0, 0]
            },
            barWidth: "35%",
            barWidth: "40%",
            label: {
              show: inboundData.length <= 8,
              show: true,
              position: "top",
              color: "#5470c6",
              fontSize: 10
              fontSize: 10,
              formatter: (params) => params.value
            }
          },
          {
@@ -402,10 +487,11 @@
            lineStyle: { width: 2, type: "solid" },
            smooth: false,
            label: {
              show: outboundData.length <= 8,
              show: true,
              position: "top",
              color: "#91cc75",
              fontSize: 10
              fontSize: 10,
              formatter: (params) => params.value
            }
          }
        ]
@@ -413,56 +499,22 @@
      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;
      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);
@@ -472,14 +524,12 @@
      const option = {
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "shadow"
          },
          formatter: function(params) {
          axisPointer: { type: "shadow" },
          formatter: (params) => {
            let tip = params[0].name + "<br/>";
            params.forEach(param => {
              const dataIndex = param.dataIndex;
              const warehouse = window.homeComponent?.warehouseData[dataIndex];
              const warehouse = this.warehouseData[dataIndex];
              if (warehouse) {
                if (param.seriesName === "已用容量") {
                  tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.hasStockPercentage})<br/>`;
@@ -519,9 +569,9 @@
              label: {
                show: true,
                position: "top",
                formatter: (params) => {
                  const pct = hasStockPercentages[params.dataIndex];
                  return `${params.value} (${pct})`;
                formatter: () => {
                  const pct = hasStockPercentages[index];
                  return `${value} (${pct})`;
                },
                color: "#91cc75",
                fontSize: 11
@@ -537,9 +587,9 @@
              label: {
                show: true,
                position: "top",
                formatter: (params) => {
                  const pct = noStockPercentages[params.dataIndex];
                  return `${params.value} (${pct})`;
                formatter: () => {
                  const pct = noStockPercentages[index];
                  return `${value} (${pct})`;
                },
                color: "#fac858",
                fontSize: 11
@@ -550,7 +600,6 @@
        ]
      };
      window.homeComponent = this;
      this.charts.warehouse.setOption(option, true);
    }
  }
@@ -566,10 +615,10 @@
  background-attachment: fixed;
}
/* KPI 卡片样式 */
/* KPI 卡片样式 - 5列布局 */
.kpi-cards {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-columns: repeat(5, 1fr);
  gap: 20px;
  margin-bottom: 24px;
}
@@ -617,24 +666,11 @@
  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 {
/* 每日图表布局 - 每行2个 */
.chart-row.daily-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin-bottom: 20px;
}
.chart-row.full-width {
  width: 100%;
  margin-bottom: 20px;
}
@@ -680,23 +716,6 @@
  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: #00ffff;
  font-size: 15px;
@@ -717,11 +736,13 @@
  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 {
@@ -733,7 +754,7 @@
.number-value {
  display: block;
  font-size: 20px;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 1px;
}
@@ -742,67 +763,41 @@
  color: #5470c6;
}
.number-item.outbound .number-value {
  color: #91cc75;
.number-item.battery .number-value {
  color: #ee6666;
}
.number-item.stock .number-value {
  color: #fac858;
.number-item.empty-tray .number-value {
  color: #fc8452;
}
.chart-content {
  height: 240px;
  height: 280px;
  width: 100%;
}
/* 全宽图表 */
.full-width .chart-card {
  width: 100%;
}
.full-width .chart-content {
  height: 350px;
}
/* 响应式调整 */
@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;
    grid-template-columns: repeat(2, 1fr);
  }
  .chart-row.top-three {
    grid-template-columns: 1fr;
  }
  .chart-row.bottom-two {
  .chart-row.daily-grid {
    grid-template-columns: 1fr;
  }
  .chart-content {
    height: 220px;
  }
  .full-width .chart-content {
    height: 280px;
    height: 240px;
  }
  .card-title {
    font-size: 13px;
    white-space: normal;
  }
  .number-value {
    font-size: 16px;
    font-size: 14px;
  }
  .number-item {
    min-width: 60px;
  }
}
/* 添加网格线效果 */
.dashboard-container::before {
  content: "";
  position: fixed;
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockInfo.vue
@@ -21,6 +21,7 @@
  palletCode: "托盘编号",
  stockStatus: "库存状态",
  locationCode: "货位编号",
  locationId: "货位id",
  outboundDate: "出库时间",
  warehouse: "仓库",
  creator: "创建人",
@@ -63,7 +64,8 @@
      mesUploadStatus: "",
      stockStatus: "",
      locationCode: "",
      locationDetails: ""
      locationDetails: "",
      locationId: "",
    });
@@ -72,6 +74,7 @@
        { field: "palletCode", title: TEXT.palletCode, type: "string" },
        { field: "stockStatus", title: TEXT.stockStatus, type: "select", dataKey: "stockStatusEmun", data: [] },
        { field: "locationCode", title: TEXT.locationCode, type: "string" },
        { field: "locationId", title: TEXT.locationId, type: "string"},
      ],
    ]);
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_Model.Models;
@@ -415,5 +416,87 @@
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 查询各仓库电池/有货数量和空托盘数量
        /// </summary>
        /// <remarks>
        /// 仓库ID规则:1=高温库, 2=常温库, 3=化成库, 6/7=极卷库
        /// <br/>
        /// 统计规则:
        /// <br/>
        /// - 高温/常温/化成库:统计 电池数量(StockStatus=6) 和 空托盘数量(StockStatus=22)
        /// <br/>
        /// - 极卷库(6/7):统计 有货数量(StockStatus≠22) 和 空托盘数量(StockStatus=22)
        /// <br/>
        /// 通过返回数据中的 StockStatus 和 Count 可以进一步查询明细电池。
        /// </remarks>
        [HttpGet("StockAndTrayCount"), AllowAnonymous]
        public async Task<WebResponseContent> StockAndTrayCount()
        {
            try
            {
                var warehouseIds = new[] { 1, 2, 3, 6, 7 };
                var warehouseNames = new Dictionary<int, string>
        {
            { 1, "高温库" },
            { 2, "常温库" },
            { 3, "化成库" },
            { 6, "极卷库" },
            { 7, "极卷库" }
        };
                var result = new List<object>();
                foreach (var warehouseId in warehouseIds)
                {
                    var warehouseName = warehouseNames.GetValueOrDefault(warehouseId, $"仓库{warehouseId}");
                    if (warehouseId == 6 || warehouseId == 7)
                    {
                        var totalCount = await _db.Queryable<Dt_StockInfo>()
                            .Where(s => s.WarehouseId == warehouseId)
                            .CountAsync();
                        var emptyTrayCount = await _db.Queryable<Dt_StockInfo>()
                            .Where(s => s.WarehouseId == warehouseId && s.StockStatus == (int)StockStatusEmun.空托盘库存)
                            .CountAsync();
                        result.Add(new
                        {
                            WarehouseId = warehouseId,
                            WarehouseName = warehouseName,
                            HasGoodsCount = totalCount - emptyTrayCount,
                            EmptyTrayCount = emptyTrayCount,
                        });
                    }
                    else
                    {
                        var batteryCount = await _db.Queryable<Dt_StockInfo>()
                            .Where(s => s.WarehouseId == warehouseId && s.StockStatus == (int)StockStatusEmun.入库完成)
                            .LeftJoin<Dt_StockInfoDetail>((s, d) => s.Id == d.StockId)
                            .CountAsync();
                        var emptyTrayCount = await _db.Queryable<Dt_StockInfo>()
                            .Where(s => s.WarehouseId == warehouseId && s.StockStatus == (int)StockStatusEmun.空托盘库存)
                            .CountAsync();
                        result.Add(new
                        {
                            WarehouseId = warehouseId,
                            WarehouseName = warehouseName,
                            BatteryCount = batteryCount,
                            EmptyTrayCount = emptyTrayCount,
                        });
                    }
                }
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"电池和空托盘数量查询失败: {ex.Message}");
            }
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
@@ -64,7 +64,7 @@
  "PDAVersion": "4",
  "WebSocketPort": 9296,
  "AutoOutboundTask": {
    "Enable": true, /// 是否启用自动出库任务
    "Enable": false, /// 是否启用自动出库任务
    "CheckIntervalSeconds": 300, /// 检查间隔(秒)
    "TargetAddresses": { /// 按巷道前缀配置目标地址(支持多出库口)
      "GW": [ "11001", "11010", "11068" ],