c1aabd3aaa92b072591fc368d81ab2cc37a0aa14..0005d58f6888dd3e4524784d1b6f103f9b1c588e
2026-03-30 wanshenmean
合并
0005d5 对比 | 目录
2026-03-30 wanshenmean
Merge branch 'master' of http://115.159.85.185:8098/r/SuZhouGuanHong/ShanMe...
1faef6 对比 | 目录
2026-03-30 wanshenmean
feat(stockChat): 库存3D查看器完整实现
848276 对比 | 目录
2026-03-30 xiazhengtongxue
feat(AGV): 新增极卷库AGV接口
f197ac 对比 | 目录
2026-03-30 wanshenmean
fix(stockChat): 修复驼峰命名并添加调试日志
5a73b7 对比 | 目录
2026-03-30 wanshenmean
fix: 添加 Warehouse.GetAll 接口并修复前端数据解析
08220d 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): OutboundFinishTaskTrayAsync添加任务和库存历史保存
85fac6 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): InboundFinishTaskTrayAsync添加任务和库存历史保存
26676b 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): 任务完成方法传入正确的OperateType
782ace 对比 | 目录
2026-03-30 wanshenmean
refactor(StockInfo): 重构 Get3DLayoutAsync 使用依赖注入服务
1f15dd 对比 | 目录
2026-03-30 wanshenmean
fix(StockInfoService): 修复 Get3DLayoutAsync 方法中 Details null 引用问题
b430ac 对比 | 目录
2026-03-30 wanshenmean
feat(stockChat): 集成 SignalR 实现实时库存更新
715cf4 对比 | 目录
2026-03-30 wanshenmean
feat(stockChat): 添加库存3D查看器扩展配置文件
1bd226 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): CompleteTaskAsync添加任务历史保存逻辑
e4f684 对比 | 目录
2026-03-30 wanshenmean
feat(stockChat): create 3D warehouse visualization component
14980b 对比 | 目录
2026-03-30 wanshenmean
feat(router): register /stockChat route
77531e 对比 | 目录
2026-03-30 wanshenmean
feat(WMSClient): add three.js dependency for 3D visualization
019ad3 对比 | 目录
2026-03-30 wanshenmean
feat(SignalR): 创建 StockHub 实现库存实时更新
20529b 对比 | 目录
2026-03-30 wanshenmean
feat(StockInfoController): 添加 Get3DLayout GET 接口
f1f954 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): 注入ITask_HtyService和IStockInfo_HtyService
1dcd4d 对比 | 目录
2026-03-30 wanshenmean
feat(StockInfoService): 实现 Get3DLayoutAsync 方法
a87dfb 对比 | 目录
2026-03-30 wanshenmean
docs: 添加任务库存历史记录实现计划
0a297d 对比 | 目录
2026-03-30 wanshenmean
feat(IStockInfoService): 新增 Get3DLayoutAsync 方法签名
2d27e6 对比 | 目录
2026-03-30 wanshenmean
feat(Stock3DLayoutDTO): 新增库存3D布局数据传输对象
fffa2d 对比 | 目录
2026-03-30 wanshenmean
docs: 添加任务库存历史记录设计
5fbc21 对比 | 目录
2026-03-30 wanshenmean
docs: 修复设计文档剩余问题
60a6b9 对比 | 目录
2026-03-30 wanshenmean
docs: 修复库存3D查看器设计文档问题
a7b66e 对比 | 目录
2026-03-30 wanshenmean
docs: 添加库存3D查看器设计文档
b698e3 对比 | 目录
2026-03-30 wanshenmean
fix(Home): 修正图表类型,本月趋势改为柱状+折线组合,库龄分布改为柱状图
2067fa 对比 | 目录
2026-03-30 wanshenmean
feat(Home): 重写首页为仪表盘图表页面
470a83 对比 | 目录
2026-03-30 wanshenmean
fix(Dashboard): 添加错误处理,统一DateTime使用
6af451 对比 | 目录
2026-03-30 wanshenmean
feat(Dashboard): 添加仪表盘控制器,包含6个统计接口
0a94fa 对比 | 目录
2026-03-30 wanshenmean
docs: 修复前端 methods 块重复定义问题
f0704a 对比 | 目录
2026-03-30 wanshenmean
docs: 修正实现计划中的问题
b9b411 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): 新增OutboundFinishTaskTrayAsync空托盘出库完成方法
7ddfdc 对比 | 目录
2026-03-30 wanshenmean
docs: 添加首页仪表盘实现计划
42c82b 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): 新增InboundFinishTaskTrayAsync空托盘入库完成方法
468e3c 对比 | 目录
2026-03-30 wanshenmean
docs: 修正TaskType枚举值范围与判断条件一致
56bc70 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): OutboundFinishTaskAsync添加MES出站调用
263ab8 对比 | 目录
2026-03-30 wanshenmean
docs: 修正仪表盘设计文档中的关键错误
c200bc 对比 | 目录
2026-03-30 wanshenmean
docs: 添加首页仪表盘图表功能设计
8f3d4c 对比 | 目录
2026-03-30 wanshenmean
feat(TaskService): InboundFinishTaskAsync添加MES进站调用
85fdfa 对比 | 目录
2026-03-30 wanshenmean
docs: 添加 MES 托盘进站出站集成实现计划
cff279 对比 | 目录
2026-03-30 wanshenmean
docs: 添加 MES 托盘进站出站集成设计
01d5cf 对比 | 目录
2026-03-30 wanshenmean
feat(StockService): SplitPalletAsync添加MES解绑调用
ddb526 对比 | 目录
2026-03-30 wanshenmean
fix(StockService): 调整ChangePalletAsync的MES解绑顺序为换出前
63d0a6 对比 | 目录
2026-03-30 wanshenmean
feat(StockService): ChangePalletAsync添加MES解绑和绑定调用
1a8dc6 对比 | 目录
2026-03-30 wanshenmean
fix(StockService): 修复MES成功判断逻辑 - 检查Data.IsSuccess而非HTTP层IsSuccess
6a70c3 对比 | 目录
2026-03-30 wanshenmean
fix(StockService): 调整MES调用顺序为先WMS写入再调用MES
4a36b0 对比 | 目录
2026-03-30 wanshenmean
fix(WIDESEA_StockService): 添加缺失的WIDESEA_IBasicService项目引用
484a4a 对比 | 目录
已添加17个文件
已修改21个文件
6188 ■■■■■ 文件已修改
Code/WMS/WIDESEA_WMSClient/package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/pnpm-lock.yaml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/extension/stock/stockChat.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/router/viewGird.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue 407 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue 684 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/taskinfo/task.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.claude/settings.local.json 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/LocationEnum/LocationTypeEnum.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/WareHouseEnum/WarehouseEnum.cs 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/AGVTaskDto.cs 636 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockInfoService.cs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITask_HtyService.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/WIDESEA_StockService.csproj 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 596 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/Task_HtyService.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Basic/WarehouseController.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/plans/2026-03-30-MES托盘进站出站集成实现计划.md 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/plans/2026-03-30-MES电芯绑定解绑集成实现计划.md 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/plans/2026-03-30-dashboard-chart-plan.md 837 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/plans/2026-03-30-任务库存历史记录实现计划.md 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/specs/2026-03-30-MES托盘进站出站集成设计.md 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/specs/2026-03-30-MES电芯绑定解绑集成设计.md 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/specs/2026-03-30-dashboard-chart-design.md 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/specs/2026-03-30-stock-chat-3d-design.md 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/docs/superpowers/specs/2026-03-30-任务库存历史记录设计.md 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/package.json
@@ -20,6 +20,7 @@
    "jsbarcode": "^3.11.6",
    "less": "^4.1.1",
    "qrcode": "^1.5.4",
    "three": "^0.160.0",
    "vue": "^3.2.37",
    "vue-barcode": "^1.3.0",
    "vue-draggable-next": "^2.0.1",
Code/WMS/WIDESEA_WMSClient/pnpm-lock.yaml
@@ -38,6 +38,9 @@
      qrcode:
        specifier: ^1.5.4
        version: 1.5.4
      three:
        specifier: ^0.160.0
        version: 0.160.1
      vue:
        specifier: ^3.2.37
        version: 3.5.30
@@ -1811,6 +1814,9 @@
  thenify@3.3.1:
    resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
  three@0.160.1:
    resolution: {integrity: sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==}
  through@2.3.8:
    resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -3681,6 +3687,8 @@
    dependencies:
      any-promise: 1.3.0
  three@0.160.1: {}
  through@2.3.8: {}
  to-arraybuffer@1.0.1: {}
Code/WMS/WIDESEA_WMSClient/src/extension/stock/stockChat.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
// stockChat.js - åº“å­˜3D查看器扩展配置
let extension = {
  components: {
    gridHeader: '',
    gridBody: '',
    gridFooter: '',
    modelHeader: '',
    modelBody: '',
    modelFooter: ''
  },
  tableAction: '',
  buttons: { view: [], box: [], detail: [] },
  methods: {
    onInit() {
      // æ‰©å±•初始化
    },
    onInited() {
      // æ¡†æž¶åˆå§‹åŒ–完成后
    }
  }
};
export default extension;
Code/WMS/WIDESEA_WMSClient/src/router/viewGird.js
@@ -94,6 +94,11 @@
    path: '/stockView',
    name: 'stockView',
    component: () => import('@/views/stock/stockView.vue')
  }
  , {
    path: '/stockChat',
    name: 'stockChat',
    component: () => import('@/views/stock/stockChat.vue')
  }, {
    path: '/stockQuantityChangeRecord',
    name: 'stockQuantityChangeRecord',
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue
@@ -1,24 +1,411 @@
<template>
  <div class="title"></div>
  <div class="dashboard-container">
    <!-- é¡¶éƒ¨ï¼šæœ¬æœˆå‡ºå…¥åº“趋势 (全宽) -->
    <div class="chart-row full-width">
      <div class="chart-card">
        <div class="card-title">本月出入库趋势</div>
        <div id="chart-monthly-trend" class="chart-content"></div>
      </div>
    </div>
    <!-- ç¬¬äºŒè¡Œï¼šä»Šæ—¥/本周出入库对比 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">今日出入库对比</div>
        <div id="chart-today" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">本周出入库对比</div>
        <div id="chart-week" class="chart-content"></div>
      </div>
    </div>
    <!-- ç¬¬ä¸‰è¡Œï¼šæœ¬æœˆå¯¹æ¯”/库存总量 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">本月出入库对比</div>
        <div id="chart-month" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">当前库存总量</div>
        <div class="stock-total">
          <div class="total-number">{{ overviewData.TotalStock || 0 }}</div>
          <div class="total-label">托盘</div>
        </div>
      </div>
    </div>
    <!-- ç¬¬å››è¡Œï¼šåº“龄分布/仓库分布 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">库存库龄分布</div>
        <div id="chart-stock-age" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">各仓库库存分布</div>
        <div id="chart-warehouse" class="chart-content"></div>
      </div>
    </div>
  </div>
</template>
<script>
import { ref, reactive } from 'vue'
import * as echarts from "echarts";
export default {
  setup() {
  name: "Home",
  data() {
    return {
      charts: {},
      overviewData: {
        TodayInbound: 0,
        TodayOutbound: 0,
        MonthInbound: 0,
        MonthOutbound: 0,
        TotalStock: 0
      },
      weeklyData: [],
      monthlyData: [],
      stockAgeData: [],
      warehouseData: []
    };
  },
  mounted() {
    this.initCharts();
    this.loadData();
    window.addEventListener("resize", this.handleResize);
  },
  beforeUnmount() {
    window.removeEventListener("resize", this.handleResize);
    Object.values(this.charts).forEach(chart => chart.dispose());
  },
  methods: {
    handleResize() {
      Object.values(this.charts).forEach(chart => chart.resize());
    },
    initCharts() {
      this.charts.monthlyTrend = echarts.init(document.getElementById("chart-monthly-trend"));
      this.charts.today = echarts.init(document.getElementById("chart-today"));
      this.charts.week = echarts.init(document.getElementById("chart-week"));
      this.charts.month = echarts.init(document.getElementById("chart-month"));
      this.charts.stockAge = echarts.init(document.getElementById("chart-stock-age"));
      this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse"));
    },
    async loadData() {
      await this.loadOverview();
      await this.loadWeeklyStats();
      await this.loadMonthlyStats();
      await this.loadStockAgeDistribution();
      await this.loadStockByWarehouse();
    },
    async loadOverview() {
      try {
        const res = await this.http.get("/api/Dashboard/Overview");
        if (res.Status && res.Data) {
          this.overviewData = res.Data;
          this.updateTodayChart();
          this.updateWeekChart();
          this.updateMonthChart();
        }
      } catch (e) {
        console.error("加载总览数据失败", e);
      }
    },
    async loadWeeklyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 });
        if (res.Status && res.Data) {
          this.weeklyData = res.Data;
          this.updateWeekChart();
        }
      } catch (e) {
        console.error("加载每周统计失败", e);
      }
    },
    async loadMonthlyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/MonthlyStats", { months: 12 });
        if (res.Status && res.Data) {
          this.monthlyData = res.Data;
          this.updateMonthlyTrendChart();
        }
      } catch (e) {
        console.error("加载每月统计失败", e);
      }
    },
    async loadStockAgeDistribution() {
      try {
        const res = await this.http.get("/api/Dashboard/StockAgeDistribution");
        if (res.Status && res.Data) {
          this.stockAgeData = res.Data;
          this.updateStockAgeChart();
        }
      } catch (e) {
        console.error("加载库龄分布失败", e);
      }
    },
    async loadStockByWarehouse() {
      try {
        const res = await this.http.get("/api/Dashboard/StockByWarehouse");
        if (res.Status && res.Data) {
          this.warehouseData = res.Data;
          this.updateWarehouseChart();
        }
      } catch (e) {
        console.error("加载仓库分布失败", e);
      }
    },
    updateTodayChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["今日"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.TodayInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.TodayOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.today.setOption(option, true);
    },
    updateWeekChart() {
      const thisWeek = this.getThisWeekData(this.weeklyData);
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本周"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [thisWeek.Inbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [thisWeek.Outbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.week.setOption(option, true);
    },
    getThisWeekData(weeklyData) {
      if (!weeklyData || weeklyData.length === 0) return { Inbound: 0, Outbound: 0 };
      const thisWeekKey = this.getCurrentWeekKey();
      const thisWeek = weeklyData.find(w => w.Week === thisWeekKey);
      return thisWeek || { Inbound: 0, Outbound: 0 };
    },
    getCurrentWeekKey() {
      const now = new Date();
      const diff = (7 + (now.getDay() - 1)) % 7;
      const monday = new Date(now);
      monday.setDate(now.getDate() - diff);
      const year = monday.getFullYear();
      const jan1 = new Date(year, 0, 1);
      const weekNum = Math.ceil(((monday - jan1) / 86400000 + jan1.getDay() + 1) / 7);
      return `${year}-W${String(weekNum).padStart(2, "0")}`;
    },
    updateMonthChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本月"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.MonthInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.MonthOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.month.setOption(option, true);
    },
    updateMonthlyTrendChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.monthlyData.map(m => m.Month),
          axisLabel: { color: "#fff", rotate: 45 }
        },
        yAxis: [
          {
            type: "value",
            name: "数量",
            axisLabel: { color: "#fff" }
          }
        ],
        series: [
          { name: "入库", type: "bar", data: this.monthlyData.map(m => m.Inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "line", data: this.monthlyData.map(m => m.Outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.monthlyTrend.setOption(option, true);
    },
    updateStockAgeChart() {
      const option = {
        tooltip: { trigger: "axis" },
        xAxis: {
          type: "category",
          data: this.stockAgeData.map(s => s.Range),
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          {
            type: "bar",
            data: this.stockAgeData.map(s => s.Count),
            itemStyle: { color: "#5470c6" }
          }
        ]
      };
      this.charts.stockAge.setOption(option, true);
    },
    updateWarehouseChart() {
      const option = {
        tooltip: { trigger: "axis" },
        xAxis: {
          type: "category",
          data: this.warehouseData.map(w => w.Warehouse),
          axisLabel: { color: "#fff", rotate: 30 }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          {
            type: "bar",
            data: this.warehouseData.map(w => w.Count),
            itemStyle: { color: "#5470c6" }
          }
        ]
      };
      this.charts.warehouse.setOption(option, true);
    }
  }
}
};
</script>
<style scoped>
.title {
  line-height: 70vh;
  text-align: center;
  font-size: 28px;
  color: orange;
.dashboard-container {
  padding: 20px;
  background-color: #0e1a2b;
  min-height: calc(100vh - 60px);
}
</style>
.chart-row {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}
.chart-row.full-width {
  width: 100%;
}
.chart-card {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(25, 186, 139, 0.17);
  border-radius: 4px;
  padding: 15px;
  position: relative;
}
.chart-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-left: 2px solid #02a6b5;
}
.chart-card::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-right: 2px solid #02a6b5;
}
.card-title {
  color: #fff;
  font-size: 16px;
  text-align: center;
  margin-bottom: 10px;
}
.chart-content {
  height: 280px;
  width: 100%;
}
.stock-total {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 280px;
}
.total-number {
  font-size: 64px;
  font-weight: bold;
  color: #67caca;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
}
.total-label {
  font-size: 18px;
  color: #fcf0d8;
  margin-top: 10px;
}
/* å…¨å®½å›¾è¡¨ */
.full-width .chart-card {
  flex: none;
  width: 100%;
}
.full-width .chart-content {
  height: 350px;
}
</style>
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,684 @@
<template>
  <div class="stock-chat-container">
    <!-- ä»“库 Tabs -->
    <el-tabs v-model="activeWarehouse" @tab-change="onWarehouseChange">
      <el-tab-pane
        v-for="wh in warehouseList"
        :key="wh.warehouseId || wh.id"
        :label="wh.warehouseName"
        :name="wh.warehouseId || wh.id"
      />
    </el-tabs>
    <!-- å·¥å…·æ  -->
    <div class="toolbar">
      <el-select v-model="filterStockStatus" placeholder="库存状态筛选" style="width: 160px" clearable>
        <el-option label="组盘暂存(1)" :value="1" />
        <el-option label="入库确认(3)" :value="3" />
        <el-option label="入库完成(6)" :value="6" />
        <el-option label="出库锁定(7)" :value="7" />
        <el-option label="出库完成(8)" :value="8" />
        <el-option label="空托盘(22)" :value="22" />
      </el-select>
      <el-select v-model="filterMaterielCode" placeholder="物料筛选" style="width: 140px" clearable>
        <el-option v-for="code in materielCodeList" :key="code" :label="code" :value="code" />
      </el-select>
      <el-select v-model="filterBatchNo" placeholder="批次筛选" style="width: 140px" clearable>
        <el-option v-for="batch in batchNoList" :key="batch" :label="batch" :value="batch" />
      </el-select>
      <el-button @click="resetCamera">重置视角</el-button>
      <el-button type="primary" @click="refreshData" :loading="refreshing">刷新数据</el-button>
    </div>
    <!-- 3D Canvas -->
    <div ref="canvasContainer" class="canvas-container" />
    <!-- çŠ¶æ€å›¾ä¾‹ -->
    <div class="legend">
      <div class="legend-title">货位状态</div>
      <div v-for="item in legendItems" :key="item.status" class="legend-item">
        <span class="color-box" :style="{ background: item.color }" />
        <span>{{ item.label }}</span>
      </div>
    </div>
    <!-- è¯¦æƒ…侧边面板 -->
    <el-drawer v-model="detailDialogVisible" title="库存详情" direction="rtl" size="500px">
      <div v-if="selectedLocation" class="detail-content">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="货位编号">{{ selectedLocation.locationCode }}</el-descriptions-item>
          <el-descriptions-item label="货位状态">{{ getLocationStatusText(selectedLocation.locationStatus) }}</el-descriptions-item>
          <el-descriptions-item label="托盘编号">{{ selectedLocation.palletCode || '无' }}</el-descriptions-item>
          <el-descriptions-item label="库存状态">{{ getStockStatusText(selectedLocation.stockStatus) }}</el-descriptions-item>
          <el-descriptions-item label="总库存">{{ selectedLocation.stockQuantity }}{{ selectedLocation.unit || '' }}</el-descriptions-item>
        </el-descriptions>
        <!-- åº“存明细表格 -->
        <div v-if="selectedLocation.details && selectedLocation.details.length > 0" class="detail-table">
          <h4>库存明细</h4>
          <el-table :data="selectedLocation.details" border size="small" max-height="400">
            <el-table-column prop="materielCode" label="物料编码" width="100" />
            <el-table-column prop="materielName" label="物料名称" min-width="120" show-overflow-tooltip />
            <el-table-column prop="batchNo" label="批次号" width="100" show-overflow-tooltip />
            <el-table-column prop="stockQuantity" label="数量" width="70" align="right" />
            <el-table-column prop="unit" label="单位" width="50" align="center" />
            <el-table-column prop="effectiveDate" label="有效期" width="100" />
          </el-table>
        </div>
        <div v-else class="no-detail">
          <el-empty description="暂无库存明细" />
        </div>
      </div>
    </el-drawer>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as signalR from '@microsoft/signalr'
const { proxy } = getCurrentInstance()
// SignalR è¿žæŽ¥
let connection = null
// é¢œè‰²å¸¸é‡ - åŸºäºŽå®žé™…枚举值
// è´§ä½çŠ¶æ€(locationStatus): 0=空闲, 1=锁定, 10=有货锁定, 20=空闲锁定, 99=大托盘锁定, 100=有货
// åº“存状态(stockStatus): 1=组盘暂存, 3=入库确认, 6=入库完成, 7=出库锁定, 8=出库完成, 9=移库锁定等
const COLOR_MAP = {
  // è´§ä½çŠ¶æ€é¢œè‰²
  LOC_FREE: 0x90EE90,         // 0=空闲 - æµ…绿色
  LOC_LOCK: 0xFF6B6B,        // 1=锁定 - çº¢è‰²
  LOC_INSTOCK_LOCK: 0xFFA500, // 10=有货锁定 - æ©™è‰²
  LOC_FREE_LOCK: 0xFFD700,   // 20=空闲锁定 - é‡‘色
  LOC_PALLET_LOCK: 0x9370DB,  // 99=大托盘锁定 - ç´«è‰²
  LOC_INSTOCK: 0x409EFF,     // 100=有货 - è“è‰²
  // åº“存状态颜色
  STOCK_PENDING: 0x00CED1,      // 1=组盘暂存 - æ·±é’色
  STOCK_CONFIRMED: 0x87CEEB,     // 3=入库确认 - å¤©è“è‰²
  STOCK_COMPLETED: 0x32CD32,     // 6=入库完成 - äº®ç»¿è‰²
  STOCK_OUT_LOCK: 0xFF6347,     // 7=出库锁定 - ç•ªèŒ„红
  STOCK_OUT_COMPLETED: 0x228B22, // 8=出库完成 - æ£®æž—绿
  STOCK_TRANSFER_LOCK: 0xFF8C00, // 9=移库锁定 - æ·±æ©™è‰²
  STOCK_COMPLETED_NO_ORDER: 0x20B2AA, // 10=入库完成未建出库单
  STOCK_RETURN: 0xFF4500,       // 11=退库
  STOCK_MANUAL_PENDING: 0x48D1CC, // 12=手动组盘暂存
  STOCK_MANUAL_CONFIRMED: 0x7FFFD4, // 13=手动组盘入库确认
  STOCK_PICK_COMPLETED: 0x6B8E23, // 14=拣选完成
  STOCK_MES_RETURN: 0xDC143C,   // 21=MES退库
  STOCK_EMPTY_TRAY: 0xDDA0DD,   // 22=空托盘库存 - æ¢…红色
  STOCK_GROUP_CANCEL: 0xDEB887, // 99=组盘撤销 - æš—金色
  STOCK_IN_CANCEL: 0xA0522D,    // 199=入库撤销 - èµ­è‰²
}
// å›¾ä¾‹é¡¹ - è´§ä½çŠ¶æ€
const legendItems = [
  { status: 'loc_free', label: '空闲(0)', color: '#90EE90' },
  { status: 'loc_lock', label: '锁定(1)', color: '#FF6B6B' },
  { status: 'loc_instock_lock', label: '有货锁定(10)', color: '#FFA500' },
  { status: 'loc_free_lock', label: '空闲锁定(20)', color: '#FFD700' },
  { status: 'loc_pallet_lock', label: '大托盘锁定(99)', color: '#9370DB' },
  { status: 'loc_instock', label: '有货(100)', color: '#409EFF' },
]
// Refs
const canvasContainer = ref(null)
// çŠ¶æ€
const activeWarehouse = ref(null)
const warehouseList = ref([])
const filterStockStatus = ref(null)
const filterMaterielCode = ref(null)
const filterBatchNo = ref(null)
const materielCodeList = ref([])
const batchNoList = ref([])
const detailDialogVisible = ref(false)
const selectedLocation = ref(null)
const refreshing = ref(false)
// Three.js ç›¸å…³
let scene, camera, renderer, controls, raycaster, mouse
let locationMesh = null
let locationData = []
let originalLocationData = [] // ä¿å­˜åŽŸå§‹å®Œæ•´æ•°æ®ï¼Œç”¨äºŽç­›é€‰æ¢å¤
let animationId = null
let locationIdToInstanceId = new Map() // locationId -> instanceId æ˜ å°„
// SignalR åˆå§‹åŒ–
function initSignalR() {
  proxy.http.post('api/User/GetCurrentUserInfo').then((result) => {
    connection = new signalR.HubConnectionBuilder()
      .withAutomaticReconnect()
      .withUrl(`${proxy.http.ipAddress}stockHub?userName=${result.data.userName}`)
      .build();
    connection.start().catch((err) => console.log('SignalR连接失败:', err));
    connection.on('StockUpdated', (update) => {
      console.log('收到库存更新:', update)
      // æ›´æ–°å¯¹åº”货位的数据
      const idx = locationData.findIndex(x => x.locationId === update.locationId);
      if (idx !== -1) {
        locationData[idx].stockQuantity = update.stockQuantity;
        locationData[idx].stockStatus = update.stockStatus;
        locationData[idx].palletCode = update.palletCode;
        locationData[idx].locationStatus = update.locationStatus;
        // æ›´æ–°åº“存明细
        if (update.details && update.details.length > 0) {
          locationData[idx].details = update.details;
        }
        // é€šè¿‡æ˜ å°„找到实例ID,更新颜色
        const instanceId = locationIdToInstanceId.get(update.locationId);
        if (instanceId !== undefined) {
          updateInstanceColor(instanceId, update.locationStatus);
        }
      }
    });
  });
}
// æ›´æ–°å•个货位颜色
function updateInstanceColor(instanceId, locationStatus) {
  if (!locationMesh) return;
  // æ ¹æ®è´§ä½çŠ¶æ€èŽ·å–é¢œè‰²
  const color = getLocationColorByStatus(locationStatus);
  locationMesh.setColorAt(instanceId, new THREE.Color(color));
  locationMesh.instanceColor.needsUpdate = true;
}
// æ ¹æ®è´§ä½çŠ¶æ€èŽ·å–é¢œè‰²
function getLocationColorByStatus(locStatus) {
  switch (locStatus) {
    case 0: return COLOR_MAP.LOC_FREE           // ç©ºé—²
    case 1: return COLOR_MAP.LOC_LOCK            // é”å®š
    case 10: return COLOR_MAP.LOC_INSTOCK_LOCK   // æœ‰è´§é”å®š
    case 20: return COLOR_MAP.LOC_FREE_LOCK      // ç©ºé—²é”å®š
    case 99: return COLOR_MAP.LOC_PALLET_LOCK    // å¤§æ‰˜ç›˜é”å®š
    case 100: return COLOR_MAP.LOC_INSTOCK      // æœ‰è´§
    default: return COLOR_MAP.LOC_FREE // é»˜è®¤ç©ºé—²è‰²
  }
}
// èŽ·å–è´§ä½é¢œè‰² - åªæ ¹æ®è´§ä½çŠ¶æ€
function getLocationColor(location) {
  const locStatus = location.locationStatus
  // æ ¹æ®è´§ä½çŠ¶æ€åˆ¤æ–­é¢œè‰²
  switch (locStatus) {
    case 0: return COLOR_MAP.LOC_FREE           // ç©ºé—²
    case 1: return COLOR_MAP.LOC_LOCK            // é”å®š
    case 10: return COLOR_MAP.LOC_INSTOCK_LOCK   // æœ‰è´§é”å®š
    case 20: return COLOR_MAP.LOC_FREE_LOCK      // ç©ºé—²é”å®š
    case 99: return COLOR_MAP.LOC_PALLET_LOCK    // å¤§æ‰˜ç›˜é”å®š
    case 100: return COLOR_MAP.LOC_INSTOCK      // æœ‰è´§
    default: return COLOR_MAP.LOC_FREE // é»˜è®¤ç©ºé—²è‰²
  }
}
// èŽ·å–è´§ä½çŠ¶æ€æ–‡æœ¬
function getLocationStatusText(status) {
  const map = {
    0: '空闲',
    1: '锁定',
    10: '有货锁定',
    20: '空闲锁定',
    99: '大托盘锁定',
    100: '有货'
  }
  return map[status] || '未知(' + status + ')'
}
// èŽ·å–åº“å­˜çŠ¶æ€æ–‡æœ¬
function getStockStatusText(status) {
  const map = {
    0: '无库存',
    1: '组盘暂存',
    3: '入库确认',
    6: '入库完成',
    7: '出库锁定',
    8: '出库完成',
    9: '移库锁定',
    10: '入库完成未建出库单',
    11: '退库',
    12: '手动组盘暂存',
    13: '手动组盘入库确认',
    14: '拣选完成',
    21: 'MES退库',
    22: '空托盘库存',
    99: '组盘撤销',
    199: '入库撤销'
  }
  return map[status] || '未知(' + status + ')'
}
// åŠ è½½ä»“åº“åˆ—è¡¨
async function loadWarehouseList() {
  try {
    const res = await proxy.http.get('/api/Warehouse/GetAll')
    if (res.status && res.data) {
      warehouseList.value = res.data
      if (res.data.length > 0) {
        activeWarehouse.value = res.data[0].warehouseId || res.data[0].id
        await loadWarehouseData(activeWarehouse.value)
      }
    }
  } catch (e) {
    console.error('加载仓库列表失败', e)
  }
}
// åŠ è½½ä»“åº“è´§ä½æ•°æ®
async function loadWarehouseData(warehouseId) {
  try {
    const res = await proxy.http.get(`/api/StockInfo/Get3DLayout?warehouseId=${warehouseId}`)
    if (res.status && res.data) {
      const data = res.data
      originalLocationData = data.locations || [] // ä¿å­˜åŽŸå§‹å®Œæ•´æ•°æ®
      locationData = [...originalLocationData] // å½“前显示数据
      // ä½¿ç”¨åŽç«¯è¿”回的筛选列表
      materielCodeList.value = data.materielCodeList || []
      batchNoList.value = data.batchNoList || []
      // æ¸²æŸ“货位
      renderLocations()
    }
  } catch (e) {
    console.error('加载货位数据失败', e)
  }
}
// åˆå§‹åŒ– Three.js åœºæ™¯
function initThreeJS() {
  if (!canvasContainer.value) return
  const width = canvasContainer.value.clientWidth
  const height = canvasContainer.value.clientHeight
  // åˆ›å»ºåœºæ™¯
  scene = new THREE.Scene()
  scene.background = new THREE.Color(0x1a1a2e)
  // åˆ›å»ºç›¸æœº
  camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
  camera.position.set(20, 20, 20)
  camera.lookAt(0, 0, 0)
  // åˆ›å»ºæ¸²æŸ“器
  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(width, height)
  canvasContainer.value.appendChild(renderer.domElement)
  // åˆ›å»ºè½¨é“控制器
  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true
  controls.dampingFactor = 0.05
  // åˆ›å»ºçŽ¯å¢ƒå…‰
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(ambientLight)
  // åˆ›å»ºä¸»å®šå‘å…‰
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
  directionalLight.position.set(50, 100, 50)
  scene.add(directionalLight)
  // åˆ›å»ºè¡¥å…‰
  const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
  fillLight.position.set(-50, 50, -50)
  scene.add(fillLight)
  // åˆ›å»ºåœ°é¢
  const groundGeometry = new THREE.PlaneGeometry(100, 100)
  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x2d2d2d })
  const ground = new THREE.Mesh(groundGeometry, groundMaterial)
  ground.rotation.x = -Math.PI / 2
  ground.position.y = -0.5
  scene.add(ground)
  // åˆ›å»ºç½‘æ ¼
  const gridHelper = new THREE.GridHelper(100, 50)
  scene.add(gridHelper)
  // åˆ›å»ºå°„线检测器
  raycaster = new THREE.Raycaster()
  mouse = new THREE.Vector2()
  // æ·»åŠ ç‚¹å‡»äº‹ä»¶
  canvasContainer.value.addEventListener('mousedown', onCanvasClick)
  // å¼€å§‹æ¸²æŸ“循环
  animate()
}
// æ¸²æŸ“货位
function renderLocations() {
  if (!scene) return
  console.log('渲染货位,原始数据总数:', originalLocationData.length)
  // å¦‚果原始数据为空,尝试重新加载
  if (originalLocationData.length === 0) {
    console.warn('原始数据为空,重新加载...')
    if (activeWarehouse.value) {
      loadWarehouseData(activeWarehouse.value)
    }
    return
  }
  // ç§»é™¤æ—§çš„货位网格
  console.log("🚀 ~ renderLocations ~ locationMesh:", locationMesh)
  if (locationMesh) {
    scene.remove(locationMesh)
    locationMesh.geometry.dispose()
    if (Array.isArray(locationMesh.material)) {
      locationMesh.material.forEach(m => m.dispose())
    } else {
      locationMesh.material.dispose()
    }
    locationMesh = null
  }
  // è¿‡æ»¤æ•°æ® - å§‹ç»ˆä»ŽåŽŸå§‹å®Œæ•´æ•°æ®è¿‡æ»¤
  let filteredData = [...originalLocationData]
  if (filterStockStatus.value !== null) {
    filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
  }
  if (filterMaterielCode.value) {
    filteredData = filteredData.filter(loc => loc.materielCode === filterMaterielCode.value)
  }
  if (filterBatchNo.value) {
    filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
  }
  console.log('过滤后数据数量:', filteredData.length, '筛选条件:', filterStockStatus.value, filterMaterielCode.value, filterBatchNo.value)
  // åˆ›å»º InstancedMesh
  const geometry = new THREE.BoxGeometry(1.5, 1, 1.5)
  const material = new THREE.MeshStandardMaterial({
    color: 0xffffff,
    roughness: 0.5,
    metalness: 0.1
  })
  // å¦‚果过滤后无数据,创建空的 InstancedMesh
  if (filteredData.length === 0) {
    locationMesh = new THREE.InstancedMesh(geometry, material, 0)
    scene.add(locationMesh)
    return
  }
  locationMesh = new THREE.InstancedMesh(geometry, material, filteredData.length)
  // æ¸…空并重建映射
  locationIdToInstanceId.clear()
  const dummy = new THREE.Object3D()
  const color = new THREE.Color()
  filteredData.forEach((location, i) => {
    const x = (location.column - 1) * 2
    const y = location.layer * 1.5
    const z = (location.row - 1) * 2
    dummy.position.set(x, y, z)
    dummy.updateMatrix()
    locationMesh.setMatrixAt(i, dummy.matrix)
    // è®¾ç½®é¢œè‰²
    color.setHex(getLocationColor(location))
    locationMesh.setColorAt(i, color)
    // å»ºç«‹æ˜ å°„: locationId -> instanceId (i)
    locationIdToInstanceId.set(location.locationId, i)
    if (i === 0) {
      console.log('First location:', location, { x, y, z })
    }
  })
  locationMesh.instanceMatrix.needsUpdate = true
  if (locationMesh.instanceColor) locationMesh.instanceColor.needsUpdate = true
  scene.add(locationMesh)
}
// åŠ¨ç”»å¾ªçŽ¯
function animate() {
  animationId = requestAnimationFrame(animate)
  controls.update()
  renderer.render(scene, camera)
}
// ç‚¹å‡»ç”»å¸ƒ
function onCanvasClick(event) {
  if (!canvasContainer.value || !locationMesh) return
  const rect = canvasContainer.value.getBoundingClientRect()
  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
  raycaster.setFromCamera(mouse, camera)
  const intersects = raycaster.intersectObject(locationMesh)
  if (intersects.length > 0) {
    const instanceId = intersects[0].instanceId
    // èŽ·å–åŽŸå§‹æ•°æ®ç´¢å¼•ï¼ˆè€ƒè™‘è¿‡æ»¤åŽçš„æ•°æ®ï¼‰
    let filteredData = [...originalLocationData]
    if (filterStockStatus.value !== null) {
      filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
    }
    if (filterMaterielCode.value) {
      filteredData = filteredData.filter(loc => loc.materielCode === filterMaterielCode.value)
    }
    if (filterBatchNo.value) {
      filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
    }
    if (instanceId < filteredData.length) {
      selectedLocation.value = filteredData[instanceId]
      detailDialogVisible.value = true
      // èšç„¦ç›¸æœº
      focusCamera(selectedLocation.value)
    }
  }
}
// èšç„¦ç›¸æœºåˆ°è´§ä½
function focusCamera(location) {
  if (!camera || !controls) return
  const targetX = (location.column - 1) * 2
  const targetY = location.layer * 1.5
  const targetZ = (location.row - 1) * 2
  const offsetX = 5
  const offsetY = 5
  const offsetZ = 5
  const targetPosition = new THREE.Vector3(targetX + offsetX, targetY + offsetY, targetZ + offsetZ)
  // ä½¿ç”¨ lerp å¹³æ»‘移动
  const startPosition = camera.position.clone()
  const duration = 500
  const startTime = Date.now()
  function lerpMove() {
    const elapsed = Date.now() - startTime
    const t = Math.min(elapsed / duration, 1)
    const easeT = 1 - Math.pow(1 - t, 3) // easeOutCubic
    camera.position.lerpVectors(startPosition, targetPosition, easeT)
    controls.target.set(targetX, targetY, targetZ)
    if (t < 1) {
      requestAnimationFrame(lerpMove)
    }
  }
  lerpMove()
}
// é‡ç½®ç›¸æœº
function resetCamera() {
  if (!camera || !controls) return
  camera.position.set(20, 20, 20)
  controls.target.set(0, 0, 0)
}
// åˆ·æ–°æ•°æ®
async function refreshData() {
  refreshing.value = true
  try {
    // é‡ç½®ç­›é€‰æ¡ä»¶
    filterStockStatus.value = null
    filterMaterielCode.value = null
    filterBatchNo.value = null
    // é‡æ–°åŠ è½½å½“å‰ä»“åº“æ•°æ®
    if (activeWarehouse.value) {
      await loadWarehouseData(activeWarehouse.value)
    }
  } finally {
    refreshing.value = false
  }
}
// ä»“库切换
async function onWarehouseChange(warehouseId) {
  await loadWarehouseData(warehouseId)
}
// ç›‘听筛选变化
watch(filterStockStatus, async () => {
  await nextTick()
  if (originalLocationData.length > 0) {
    renderLocations()
  }
})
watch(filterMaterielCode, async () => {
  await nextTick()
  if (originalLocationData.length > 0) {
    renderLocations()
  }
})
watch(filterBatchNo, async () => {
  await nextTick()
  if (originalLocationData.length > 0) {
    renderLocations()
  }
})
// çª—口大小变化
function onWindowResize() {
  if (!canvasContainer.value || !camera || !renderer) return
  const width = canvasContainer.value.clientWidth
  const height = canvasContainer.value.clientHeight
  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height)
}
// ç»„件挂载
onMounted(() => {
  initThreeJS()
  loadWarehouseList()
  initSignalR()
  window.addEventListener('resize', onWindowResize)
})
// ç»„件卸载
onUnmounted(() => {
  if (animationId) {
    cancelAnimationFrame(animationId)
  }
  if (canvasContainer.value) {
    canvasContainer.value.removeEventListener('mousedown', onCanvasClick)
  }
  window.removeEventListener('resize', onWindowResize)
  if (renderer) {
    renderer.dispose()
  }
  if (connection) {
    connection.stop()
  }
})
</script>
<style scoped>
.stock-chat-container {
  width: 100%;
  height: calc(100vh - 120px);
  position: relative;
  overflow: visible;
}
.toolbar {
  position: absolute;
  top: 60px;
  left: 20px;
  z-index: 10;
  display: flex;
  gap: 10px;
  background: rgba(255, 255, 255, 0.9);
  padding: 10px;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.canvas-container {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
.legend {
  position: absolute;
  bottom: 20px;
  right: 20px;
  background: rgba(0, 0, 0, 0.7);
  padding: 10px;
  border-radius: 4px;
  color: white;
  z-index: 10;
  max-height: 400px;
  overflow-y: auto;
}
.legend-title {
  font-weight: bold;
  font-size: 13px;
  margin-bottom: 6px;
  color: #fff;
}
.legend-divider {
  height: 1px;
  background: rgba(255, 255, 255, 0.3);
  margin: 8px 0;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
  font-size: 12px;
}
.color-box {
  width: 16px;
  height: 16px;
  border-radius: 2px;
  flex-shrink: 0;
}
.detail-content {
  padding: 20px;
}
.detail-table {
  margin-top: 20px;
}
.detail-table h4 {
  margin-bottom: 10px;
  color: #303133;
}
.no-detail {
  margin-top: 20px;
}
:deep(.el-drawer__body) {
  padding: 20px;
}
</style>
Code/WMS/WIDESEA_WMSClient/src/views/taskinfo/task.vue
@@ -48,7 +48,7 @@
        { title: "创建人", field: "creater", type: "like" },
      ],
      [
        { title: "任务类型",field: "taskType",type: "selectList",dataKey: "taskType",data: [],},
        { title: "任务类型",field: "taskType",type: "selectList",dataKey: "taskTypeEnum",data: [],},
        { title: "任务状态",field: "taskStatus",type: "selectList",dataKey: "taskStatusEnum",data: [],},
        { title: "巷道号", field: "roadway", type: "like" },
      ],
@@ -96,7 +96,7 @@
        type: "int",
        width: 120,
        align: "left",
        bind: { key: "taskType", data: [] },
        bind: { key: "taskTypeEnum", data: [] },
      },
      {
        field: "taskStatus",
Code/WMS/WIDESEA_WMSServer/.claude/settings.local.json
@@ -38,7 +38,10 @@
      "Bash(cd \"E:/迅雷下载/WIDESEA_WMSServer\" && dotnet build --configuration Debug 2>&1 | tail -20)",
      "Bash(cd \"E:/迅雷下载/WIDESEA_WMSServer\" && dotnet build --configuration Debug 2>&1 | tail -25)",
      "Bash(cd \"E:/迅雷下载/WIDESEA_WMSServer\" && dotnet build --configuration Debug 2>&1 | grep -A 2 -B 2 \"error CS0\")",
      "Bash(cd \"E:/迅雷下载/WIDESEA_WMSServer\" && dotnet build --configuration Debug 2>&1 | tail -10)"
      "Bash(cd \"E:/迅雷下载/WIDESEA_WMSServer\" && dotnet build --configuration Debug 2>&1 | tail -10)",
      "Bash(dotnet build:*)",
      "Bash(git add:*)",
      "Bash(git commit:*)"
    ]
  }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/LocationEnum/LocationTypeEnum.cs
@@ -19,15 +19,15 @@
        Undefined = 0,
        /// <summary>
        /// å°æ‰˜ç›˜
        /// æ¨¡åˆ‡æ®µ
        /// </summary>
        [Description("小托盘")]
        [Description("模切段")]
        SmallPallet = 1,
        /// <summary>
        /// ä¸­æ‰˜ç›˜
        /// å·ç»•段
        /// </summary>
        [Description("中托盘")]
        [Description("卷绕段")]
        MediumPallet = 2,
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/WareHouseEnum/WarehouseEnum.cs
@@ -9,68 +9,45 @@
{
    /// <summary>
    /// ä»“库
    /// HA57 = æ·®å®‰äºŒåŽ‚ - æ¿æ–™ä»“<br/>
    /// HA58 = æ·®å®‰äºŒåŽ‚ - PP仓<br/>
    /// HA60 = æ·®å®‰äºŒåŽ‚ - è¾…料仓<br/>
    /// HA64 = æ·®å®‰äºŒåŽ‚ - æµ‹è¯•架仓<br/>
    /// HA71 = æ·®å®‰äºŒåŽ‚ - æˆå“ä»“<br/>
    /// HA72 = æ·®å®‰äºŒåŽ‚ - å°¾æ•°ä»“<br/>
    /// HA73 = æ·®å®‰äºŒåŽ‚ - ç ”发仓<br/>
    /// HA101 = æ·®å®‰äºŒåŽ‚ - æˆå“ä»“平库<br/>
    /// HA152 = æ·®å®‰äºŒåŽ‚ - å¹²è†œä»“<br/>
    /// HA153 = æ·®å®‰äºŒåŽ‚ - æ²¹å¢¨ä»“<br/>
    /// GW1 = é™•煤二厂 - é«˜æ¸©1号仓库<br/>
    /// CW1 = é™•煤二厂 - å¸¸æ¸©1号仓库<br/>
    /// HCFR1 = é™•煤二厂 - åˆ†å®¹1号仓库<br/>
    /// GW2 = é™•煤二厂 - é«˜æ¸©2号仓库<br/>
    /// ZJ1 = é™•煤二厂 - æ­£æžå·ä»“<br/>
    /// FJ1 = é™•煤二厂 - è´Ÿæžå·ä»“<br/>
    /// </summary>
    public enum WarehouseEnum
    {
        /// <summary>
        /// æ¿æ–™ä»“
        /// é«˜æ¸©1号仓库
        /// </summary>
        [Description("板料仓")]
        HA57,
        [Description("高温1号仓库")]
        GW1 = 1,
        /// <summary>
        /// PP仓
        /// å¸¸æ¸©1号仓库
        /// </summary>
        [Description("PP仓")]
        HA58,
        [Description("常温1号仓库")]
        CW1 = 2,
        /// <summary>
        /// è¾…料仓
        /// åˆ†å®¹1号仓库
        /// </summary>
        [Description("辅料仓")]
        HA60,
        [Description("分容1号仓库")]
        HCFR1 = 3,
        /// <summary>
        /// æµ‹è¯•架仓
        /// é«˜æ¸©2号仓库
        /// </summary>
        [Description("测试架仓")]
        HA64,
        [Description("高温2号仓库")]
        GW2 = 4,
        /// <summary>
        /// æˆå“ä»“
        /// æ­£æžå·ä»“
        /// </summary>
        [Description("成品仓")]
        HA71,
        [Description("正极卷仓")]
        ZJ1 = 5,
        /// <summary>
        /// å°¾æ•°ä»“
        /// è´Ÿæžå·ä»“
        /// </summary>
        [Description("尾数仓")]
        HA72,
        /// <summary>
        /// ç ”发仓
        /// </summary>
        [Description("研发仓")]
        HA73,
        /// <summary>
        /// æˆå“ä»“平库
        /// </summary>
        [Description("成品仓平库")]
        HA101,
        /// <summary>
        /// å¹²è†œä»“
        /// </summary>
        [Description("干膜仓")]
        HA152,
        /// <summary>
        /// æ²¹å¢¨ä»“
        /// </summary>
        [Description("油墨仓")]
        HA153
        [Description("负极卷仓")]
        FJ1 = 6
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
namespace WIDESEA_DTO.Stock
{
    /// <summary>
    /// åº“å­˜3D布局数据传输对象
    /// </summary>
    public class Stock3DLayoutDTO
    {
        /// <summary>
        /// ä»“库ID
        /// </summary>
        public int WarehouseId { get; set; }
        /// <summary>
        /// ä»“库名称
        /// </summary>
        public string WarehouseName { get; set; } = string.Empty;
        /// <summary>
        /// æœ€å¤§è¡Œæ•°
        /// </summary>
        public int MaxRow { get; set; }
        /// <summary>
        /// æœ€å¤§åˆ—æ•°
        /// </summary>
        public int MaxColumn { get; set; }
        /// <summary>
        /// æœ€å¤§å±‚æ•°
        /// </summary>
        public int MaxLayer { get; set; }
        /// <summary>
        /// ç‰©æ–™ç¼–码筛选列表
        /// </summary>
        public List<string> MaterielCodeList { get; set; } = new List<string>();
        /// <summary>
        /// æ‰¹æ¬¡å·ç­›é€‰åˆ—表
        /// </summary>
        public List<string> BatchNoList { get; set; } = new List<string>();
        /// <summary>
        /// è´§ä½æ•°ç»„
        /// </summary>
        public List<Location3DItemDTO> Locations { get; set; } = new List<Location3DItemDTO>();
    }
    /// <summary>
    /// 3D货位项数据传输对象
    /// </summary>
    public class Location3DItemDTO
    {
        /// <summary>
        /// è´§ä½ID
        /// </summary>
        public int LocationId { get; set; }
        /// <summary>
        /// è´§ä½ç¼–码
        /// </summary>
        public string LocationCode { get; set; } = string.Empty;
        /// <summary>
        /// è¡Œ
        /// </summary>
        public int Row { get; set; }
        /// <summary>
        /// åˆ—
        /// </summary>
        public int Column { get; set; }
        /// <summary>
        /// å±‚
        /// </summary>
        public int Layer { get; set; }
        /// <summary>
        /// è´§ä½çŠ¶æ€
        /// </summary>
        public int LocationStatus { get; set; }
        /// <summary>
        /// åº“存状态
        /// </summary>
        public int StockStatus { get; set; }
        /// <summary>
        /// åº“存数量
        /// </summary>
        public float StockQuantity { get; set; }
        /// <summary>
        /// æœ€å¤§å®¹é‡
        /// </summary>
        public float MaxCapacity { get; set; }
        /// <summary>
        /// æ‰˜ç›˜ç¼–码
        /// </summary>
        public string? PalletCode { get; set; }
        /// <summary>
        /// ç‰©æ–™ç¼–码
        /// </summary>
        public string? MaterielCode { get; set; }
        /// <summary>
        /// ç‰©æ–™åç§°
        /// </summary>
        public string? MaterielName { get; set; }
        /// <summary>
        /// æ‰¹æ¬¡å·
        /// </summary>
        public string? BatchNo { get; set; }
        /// <summary>
        /// åº“存明细列表
        /// </summary>
        public List<StockDetailItemDTO> Details { get; set; } = new();
    }
    /// <summary>
    /// åº“存明细项DTO
    /// </summary>
    public class StockDetailItemDTO
    {
        public int Id { get; set; }
        public string? MaterielCode { get; set; }
        public string? MaterielName { get; set; }
        public string? BatchNo { get; set; }
        public float StockQuantity { get; set; }
        public string? Unit { get; set; }
        public string? ProductionDate { get; set; }
        public string? EffectiveDate { get; set; }
        public string? OrderNo { get; set; }
        public int Status { get; set; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/AGVTaskDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,636 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace WIDESEA_DTO.Task
{
    public class AGVResponse
    {
        /// <summary>
        /// è¯·æ±‚结果代码 - å¿…填,true成功,false失败
        /// </summary>
        public bool Code { get; set; }
        /// <summary>
        /// è¯·æ±‚结果说明 - é€‰å¡«ï¼Œæ˜¯å¦æˆåŠŸï¼ŒæˆåŠŸè¿”å›žç©ºï¼Œå¼‚å¸¸è¿”å›žå¼‚å¸¸ä¿¡æ¯
        /// </summary>
        public string Msg { get; set; }
        /// <summary>
        /// å…¥åº“口编号 - é€‰å¡«
        /// </summary>
        public string Devid { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å· - é€‰å¡«
        /// </summary>
        public string Traynumber { get; set; }
        /// <summary>
        /// æ•´æ‰˜ç»„别-厚度 - é€‰å¡«
        /// </summary>
        public string Group { get; set; }
        /// <summary>
        /// å®½åº¦ - é€‰å¡«
        /// </summary>
        public int? Width { get; set; }
        /// <summary>
        /// æ•°æ®åˆ—表 - é€‰å¡«
        /// </summary>
        public List<string> Data { get; set; }
        /// <summary>
        /// æ¡ç å· - é€‰å¡«
        /// </summary>
        public string Labelnumber { get; set; }
        /// <summary>
        /// ç‰©æ–™ç¼–码 - é€‰å¡«
        /// </summary>
        public string Productno { get; set; }
        /// <summary>
        /// ç‰©æ–™æè¿° - é€‰å¡«
        /// </summary>
        public string Productname { get; set; }
        /// <summary>
        /// æ•°é‡ - é€‰å¡«
        /// </summary>
        public string Quantity { get; set; }
        /// <summary>
        /// å•位 - é€‰å¡«
        /// </summary>
        public string Uomcode { get; set; }
        /// <summary>
        /// ç‰©æ–™ç±»åž‹ - é€‰å¡«
        /// </summary>
        public string Producttype { get; set; }
        /// <summary>
        /// äº§å‡ºè®¾å¤‡ - é€‰å¡«
        /// </summary>
        public string Equipment { get; set; }
        /// <summary>
        /// äº§å‡ºæ—¶é—´ - é€‰å¡«
        /// </summary>
        public string Productiondate { get; set; }
        /// <summary>
        /// ä¸‹é™æ—¶é—´ - é€‰å¡«
        /// </summary>
        public string Lowerlimittime { get; set; }
        /// <summary>
        /// é¢„警时间 - é€‰å¡«
        /// </summary>
        public string Warningtime { get; set; }
        /// <summary>
        /// è¶…期时间 - é€‰å¡«
        /// </summary>
        public string Overduetime { get; set; }
        /// <summary>
        /// é¢„留自定义字段1 - é€‰å¡«
        /// </summary>
        public string Define1 { get; set; }
        /// <summary>
        /// é¢„留自定义字段2 - é€‰å¡«
        /// </summary>
        public string Define2 { get; set; }
        public AGVResponse OK(AGVDataDto aGVDataDto = null)
        {
            Msg = "";
            Code = true;
            if (aGVDataDto != null)
            {
                // å°†AGVDataDto的属性赋值给AGVResponse
                Devid = aGVDataDto.DevId;
                Traynumber = aGVDataDto.TrayNumber;
                Group = aGVDataDto.Group;
                Width = aGVDataDto.Width;
                Data = aGVDataDto.Data;
                Labelnumber = aGVDataDto.LabelNumber;
                Productno = aGVDataDto.ProductNo;
                Productname = aGVDataDto.ProductName;
                Quantity = aGVDataDto.Quantity;
                Uomcode = aGVDataDto.UomCode;
                Producttype = aGVDataDto.ProductType;
                Equipment = aGVDataDto.Equipment;
                Productiondate = aGVDataDto.ProductionDate;
                Lowerlimittime = aGVDataDto.LowerLimitTime;
                Warningtime = aGVDataDto.WarningTime;
                Overduetime = aGVDataDto.OverdueTime;
                Define1 = aGVDataDto.Define1;
                Define2 = aGVDataDto.Define2;
            }
            return this;
        }
        public AGVResponse Error(string msg)
        {
            Msg = msg;
            Code = false;
            return this;
        }
    }
    /// <summary>
    /// AGV响应数据项
    /// </summary>
    /// <summary>
    /// AGV数据DTO
    /// </summary>
    public class AGVDataDto
    {
        /// <summary>
        /// å…¥åº“口编号 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("devid")]
        [StringLength(50, ErrorMessage = "入库口编号长度不能超过50个字符")]
        public string DevId { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å· - é€‰å¡«
        /// </summary>
        [JsonPropertyName("traynumber")]
        [StringLength(50, ErrorMessage = "托盘号长度不能超过50个字符")]
        public string TrayNumber { get; set; }
        /// <summary>
        /// æ•´æ‰˜ç»„别-厚度 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("group")]
        [StringLength(50, ErrorMessage = "整托组别长度不能超过50个字符")]
        public string Group { get; set; }
        /// <summary>
        /// å®½åº¦ - é€‰å¡«
        /// </summary>
        [JsonPropertyName("width")]
        public int? Width { get; set; }
        /// <summary>
        /// æ•°æ®åˆ—表 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("data")]
        public List<string> Data { get; set; }
        /// <summary>
        /// æ¡ç å· - é€‰å¡«
        /// </summary>
        [JsonPropertyName("labelnumber")]
        [StringLength(50, ErrorMessage = "条码号长度不能超过50个字符")]
        public string LabelNumber { get; set; }
        /// <summary>
        /// ç‰©æ–™ç¼–码 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("productno")]
        [StringLength(50, ErrorMessage = "物料编码长度不能超过50个字符")]
        public string ProductNo { get; set; }
        /// <summary>
        /// ç‰©æ–™æè¿° - é€‰å¡«
        /// </summary>
        [JsonPropertyName("productname")]
        [StringLength(50, ErrorMessage = "物料描述长度不能超过50个字符")]
        public string ProductName { get; set; }
        /// <summary>
        /// æ•°é‡ - é€‰å¡«
        /// </summary>
        [JsonPropertyName("quantity")]
        [StringLength(50, ErrorMessage = "数量长度不能超过50个字符")]
        public string Quantity { get; set; }
        /// <summary>
        /// å•位 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("uomcode")]
        [StringLength(50, ErrorMessage = "单位长度不能超过50个字符")]
        public string UomCode { get; set; }
        /// <summary>
        /// ç‰©æ–™ç±»åž‹ - é€‰å¡«
        /// </summary>
        [JsonPropertyName("producttype")]
        [StringLength(50, ErrorMessage = "物料类型长度不能超过50个字符")]
        public string ProductType { get; set; }
        /// <summary>
        /// äº§å‡ºè®¾å¤‡ - é€‰å¡«
        /// </summary>
        [JsonPropertyName("equipment")]
        [StringLength(50, ErrorMessage = "产出设备长度不能超过50个字符")]
        public string Equipment { get; set; }
        /// <summary>
        /// äº§å‡ºæ—¶é—´ - é€‰å¡«
        /// </summary>
        [JsonPropertyName("productiondate")]
        [StringLength(50, ErrorMessage = "产出时间长度不能超过50个字符")]
        public string ProductionDate { get; set; }
        /// <summary>
        /// ä¸‹é™æ—¶é—´ - é€‰å¡«
        /// </summary>
        [JsonPropertyName("lowerlimittime")]
        [StringLength(50, ErrorMessage = "下限时间长度不能超过50个字符")]
        public string LowerLimitTime { get; set; }
        /// <summary>
        /// é¢„警时间 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("warningtime")]
        [StringLength(50, ErrorMessage = "预警时间长度不能超过50个字符")]
        public string WarningTime { get; set; }
        /// <summary>
        /// è¶…期时间 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("overduetime")]
        [StringLength(50, ErrorMessage = "超期时间长度不能超过50个字符")]
        public string OverdueTime { get; set; }
        /// <summary>
        /// é¢„留自定义字段1 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("define1")]
        [StringLength(50, ErrorMessage = "自定义字段1长度不能超过50个字符")]
        public string Define1 { get; set; }
        /// <summary>
        /// é¢„留自定义字段2 - é€‰å¡«
        /// </summary>
        [JsonPropertyName("define2")]
        [StringLength(50, ErrorMessage = "自定义字段2长度不能超过50个字符")]
        public string Define2 { get; set; }
    }
    /// <summary>
    /// ä»»åŠ¡åˆ›å»ºæ•°æ®ä¼ è¾“å¯¹è±¡
    /// </summary>
    public class ApplyInOutDto
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        [JsonPropertyName("taskid")]
        [Required(ErrorMessage = "任务号不能为空")]
        [StringLength(50, ErrorMessage = "任务号长度不能超过50个字符")]
        public string TaskId { get; set; }
        /// <summary>
        /// 1-模切段;2-卷绕段
        /// </summary>
        [JsonPropertyName("floor")]
        [Required(ErrorMessage = "楼层段不能为空")]
        [Range(1, 2, ErrorMessage = "楼层段值必须是1或2")]
        public int Floor { get; set; }
        /// <summary>
        /// 1-阴极;2-阳极
        /// </summary>
        [JsonPropertyName("yinyang")]
        [Required(ErrorMessage = "阴阳极不能为空")]
        [Range(1, 2, ErrorMessage = "阴阳极值必须是1或2")]
        public int YinYang { get; set; }
        /// <summary>
        /// 1-入库;2-出库
        /// </summary>
        [JsonPropertyName("inout")]
        [Required(ErrorMessage = "出入库类型不能为空")]
        [Range(1, 2, ErrorMessage = "出入库类型值必须是1或2")]
        public int InOut { get; set; }
        /// <summary>
        /// ç‰©æ–™ç±»åž‹
        /// </summary>
        [JsonPropertyName("materialtype")]
        [Required(ErrorMessage = "物料类型不能为空")]
        [StringLength(50, ErrorMessage = "物料类型长度不能超过50个字符")]
        public string MaterialType { get; set; }
        /// <summary>
        /// ç‰©æ–™æè¿°
        /// </summary>
        [JsonPropertyName("materialname")]
        [Required(ErrorMessage = "物料描述不能为空")]
        [StringLength(50, ErrorMessage = "物料描述长度不能超过50个字符")]
        public string MaterialName { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å·
        /// </summary>
        [JsonPropertyName("traynumber")]
        [StringLength(50, ErrorMessage = "托盘号长度不能超过50个字符")]
        public string TrayNumber { get; set; }
        /// <summary>
        /// æ•´æ‰˜ç»„别-厚度
        /// </summary>
        [JsonPropertyName("group")]
        [StringLength(50, ErrorMessage = "整托组别长度不能超过50个字符")]
        public string Group { get; set; }
        /// <summary>
        /// å®½åº¦
        /// </summary>
        [JsonPropertyName("width")]
        public int? Width { get; set; }
        /// <summary>
        /// æ•°æ®é›†åˆ
        /// </summary>
        [JsonPropertyName("data")]
        public List<string> Data { get; set; }
        /// <summary>
        /// æ¡ç å·
        /// </summary>
        [JsonPropertyName("labelnumber")]
        [StringLength(50, ErrorMessage = "条码号长度不能超过50个字符")]
        public string LabelNumber { get; set; }
        /// <summary>
        /// ç‰©æ–™ç¼–码
        /// </summary>
        [JsonPropertyName("productno")]
        [StringLength(50, ErrorMessage = "物料编码长度不能超过50个字符")]
        public string ProductNo { get; set; }
        /// <summary>
        /// ç‰©æ–™æè¿°
        /// </summary>
        [JsonPropertyName("productname")]
        [StringLength(50, ErrorMessage = "物料描述长度不能超过50个字符")]
        public string ProductName { get; set; }
        /// <summary>
        /// æ•°é‡
        /// </summary>
        [JsonPropertyName("quantity")]
        [StringLength(50, ErrorMessage = "数量长度不能超过50个字符")]
        public string Quantity { get; set; }
        /// <summary>
        /// å•位
        /// </summary>
        [JsonPropertyName("uomcode")]
        [StringLength(50, ErrorMessage = "单位长度不能超过50个字符")]
        public string UomCode { get; set; }
        /// <summary>
        /// ç‰©æ–™ç±»åž‹
        /// </summary>
        [JsonPropertyName("producttype")]
        [StringLength(50, ErrorMessage = "物料类型长度不能超过50个字符")]
        public string ProductType { get; set; }
        /// <summary>
        /// äº§å‡ºè®¾å¤‡
        /// </summary>
        [JsonPropertyName("equipment")]
        [StringLength(50, ErrorMessage = "产出设备长度不能超过50个字符")]
        public string Equipment { get; set; }
        /// <summary>
        /// äº§å‡ºæ—¶é—´
        /// </summary>
        [JsonPropertyName("productiondate")]
        [StringLength(50, ErrorMessage = "产出时间长度不能超过50个字符")]
        public string ProductionDate { get; set; }
        /// <summary>
        /// ä¸‹é™æ—¶é—´
        /// </summary>
        [JsonPropertyName("lowerlimittime")]
        [StringLength(50, ErrorMessage = "下限时间长度不能超过50个字符")]
        public string LowerLimitTime { get; set; }
        /// <summary>
        /// é¢„警时间
        /// </summary>
        [JsonPropertyName("warningtime")]
        [StringLength(50, ErrorMessage = "预警时间长度不能超过50个字符")]
        public string WarningTime { get; set; }
        /// <summary>
        /// è¶…期时间
        /// </summary>
        [JsonPropertyName("overduetime")]
        [StringLength(50, ErrorMessage = "超期时间长度不能超过50个字符")]
        public string OverdueTime { get; set; }
        /// <summary>
        /// é¢„留自定义字段1
        /// </summary>
        [JsonPropertyName("define1")]
        [StringLength(50, ErrorMessage = "自定义字段1长度不能超过50个字符")]
        public string Define1 { get; set; }
        /// <summary>
        /// é¢„留自定义字段2
        /// </summary>
        [JsonPropertyName("define2")]
        [StringLength(50, ErrorMessage = "自定义字段2长度不能超过50个字符")]
        public string Define2 { get; set; }
        /// <summary>
        /// è¯·æ±‚æ—¶é—´
        /// </summary>
        [JsonPropertyName("reqtime")]
        [Required(ErrorMessage = "请求时间不能为空")]
        [StringLength(50, ErrorMessage = "请求时间长度不能超过50个字符")]
        public string ReqTime { get; set; }
    }
    /// <summary>
    /// è¾“送线申请进入请求模型
    /// </summary>
    public class ApplyEnterDto
    {
        /// <summary>
        /// è®¾å¤‡ID
        /// </summary>
        [JsonPropertyName("devid")]
        [Required(ErrorMessage = "设备ID不能为空")]
        [StringLength(50, ErrorMessage = "设备ID长度不能超过50个字符")]
        public string DevId { get; set; }
        /// <summary>
        /// å‡ºå…¥åº“类型 1-入库;2-出库
        /// </summary>
        [JsonPropertyName("inout")]
        [Required(ErrorMessage = "出入库类型不能为空")]
        [Range(1, 2, ErrorMessage = "出入库类型值必须是1或2")]
        public int InOut { get; set; }
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        [JsonPropertyName("taskid")]
        [Required(ErrorMessage = "任务号不能为空")]
        [StringLength(50, ErrorMessage = "任务号长度不能超过50个字符")]
        public string TaskId { get; set; }
        /// <summary>
        /// é¢„留自定义字段1
        /// </summary>
        [JsonPropertyName("define1")]
        [StringLength(50, ErrorMessage = "自定义字段1长度不能超过50个字符")]
        public string Define1 { get; set; }
        /// <summary>
        /// é¢„留自定义字段2
        /// </summary>
        [JsonPropertyName("define2")]
        [StringLength(50, ErrorMessage = "自定义字段2长度不能超过50个字符")]
        public string Define2 { get; set; }
        /// <summary>
        /// è¯·æ±‚æ—¶é—´
        /// </summary>
        [JsonPropertyName("reqtime")]
        [Required(ErrorMessage = "请求时间不能为空")]
        [StringLength(50, ErrorMessage = "请求时间长度不能超过50个字符")]
        public string ReqTime { get; set; }
    }
    /// <summary>
    /// å‡ºåº“完成反馈
    /// </summary>
    public class OutTaskCompleteDto
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        [JsonPropertyName("taskid")]
        [Required(ErrorMessage = "任务号不能为空")]
        [StringLength(50, ErrorMessage = "任务号长度不能超过50个字符")]
        public string TaskId { get; set; }
        /// <summary>
        /// å‡ºåº“口编号
        /// </summary>
        [JsonPropertyName("devid")]
        [Required(ErrorMessage = "出库口编号不能为空")]
        [StringLength(50, ErrorMessage = "出库口编号长度不能超过50个字符")]
        public string DevId { get; set; }
        /// <summary>
        /// é¢„留自定义字段1
        /// </summary>
        [JsonPropertyName("define1")]
        [StringLength(50, ErrorMessage = "自定义字段1长度不能超过50个字符")]
        public string? Define1 { get; set; }
        /// <summary>
        /// é¢„留自定义字段2
        /// </summary>
        [JsonPropertyName("define2")]
        [StringLength(50, ErrorMessage = "自定义字段2长度不能超过50个字符")]
        public string? Define2 { get; set; }
        /// <summary>
        /// è¯·æ±‚æ—¶é—´
        /// </summary>
        [JsonPropertyName("reqtime")]
        [Required(ErrorMessage = "请求时间不能为空")]
        [StringLength(50, ErrorMessage = "请求时间长度不能超过50个字符")]
        public string ReqTime { get; set; }
    }
    /// <summary>
    /// å–放货完成请求模型
    /// </summary>
    public class TaskCompleteDto
    {
        /// <summary>
        /// è®¾å¤‡ID
        /// </summary>
        [JsonPropertyName("devid")]
        [Required(ErrorMessage = "设备ID不能为空")]
        [StringLength(50, ErrorMessage = "设备ID长度不能超过50个字符")]
        public string DevId { get; set; }
        /// <summary>
        /// å‡ºå…¥åº“类型 1-入库;2-出库
        /// </summary>
        [JsonPropertyName("inout")]
        [Required(ErrorMessage = "出入库类型不能为空")]
        [Range(1, 2, ErrorMessage = "出入库类型值必须是1或2")]
        public int InOut { get; set; }
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        [JsonPropertyName("taskid")]
        [Required(ErrorMessage = "任务号不能为空")]
        [StringLength(50, ErrorMessage = "任务号长度不能超过50个字符")]
        public string TaskId { get; set; }
        /// <summary>
        /// é¢„留自定义字段1
        /// </summary>
        [JsonPropertyName("define1")]
        [StringLength(50, ErrorMessage = "自定义字段1长度不能超过50个字符")]
        public string? Define1 { get; set; }
        /// <summary>
        /// é¢„留自定义字段2
        /// </summary>
        [JsonPropertyName("define2")]
        [StringLength(50, ErrorMessage = "自定义字段2长度不能超过50个字符")]
        public string? Define2 { get; set; }
        /// <summary>
        /// è¯·æ±‚æ—¶é—´
        /// </summary>
        [JsonPropertyName("reqtime")]
        [Required(ErrorMessage = "请求时间不能为空")]
        [StringLength(50, ErrorMessage = "请求时间长度不能超过50个字符")]
        public string ReqTime { get; set; }
    }
    /// <summary>
    /// ä»»åŠ¡å–æ¶ˆæ•°æ®ä¼ è¾“å¯¹è±¡
    /// </summary>
    public class TaskCancelDto
    {
        /// <summary>
        /// ä»»åŠ¡å·
        /// </summary>
        [JsonPropertyName("taskid")]
        [Required(ErrorMessage = "任务号不能为空")]
        [StringLength(50, ErrorMessage = "任务号长度不能超过50个字符")]
        public string TaskId { get; set; }
        /// <summary>
        /// é¢„留自定义字段1
        /// </summary>
        [JsonPropertyName("define1")]
        [StringLength(50, ErrorMessage = "自定义字段1长度不能超过50个字符")]
        public string? Define1 { get; set; }
        /// <summary>
        /// é¢„留自定义字段2
        /// </summary>
        [JsonPropertyName("define2")]
        [StringLength(50, ErrorMessage = "自定义字段2长度不能超过50个字符")]
        public string? Define2 { get; set; }
        /// <summary>
        /// è¯·æ±‚æ—¶é—´
        /// </summary>
        [JsonPropertyName("reqtime")]
        [Required(ErrorMessage = "请求时间不能为空")]
        [StringLength(50, ErrorMessage = "请求时间长度不能超过50个字符")]
        public string ReqTime { get; set; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_IStockService/IStockInfoService.cs
@@ -1,5 +1,6 @@
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_DTO.Stock;
using WIDESEA_Model.Models;
namespace WIDESEA_IStockService
@@ -48,5 +49,12 @@
        /// <param name="locationCode">货位编码</param>
        /// <returns>库存信息</returns>
        Task<Dt_StockInfo> GetStockInfoAsync(string palletCode, string locationCode);
        /// <summary>
        /// èŽ·å–ä»“åº“3D布局数据
        /// </summary>
        /// <param name="warehouseId">仓库ID</param>
        /// <returns>3D布局DTO</returns>
        Task<Stock3DLayoutDTO> Get3DLayoutAsync(int warehouseId);
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs
@@ -154,5 +154,49 @@
        /// <param name="stock"></param>
        /// <returns></returns>
        Task<WebResponseContent> CreateRobotChangePalletTaskAsync(StockDTO stock);
        #region æžå·åº“任务模块
        /// <summary>
        /// å‡ºå…¥åº“申请
        /// </summary>
        /// <param name="applyInOutDto">请求参数</param>
        /// <returns></returns>
        public Task<AGVResponse> ApplyInOutAsync(ApplyInOutDto applyInOutDto);
        /// <summary>
        /// æ‰‹åŠ¨å‡ºåº“å®Œæˆåé¦ˆç»™AGV
        /// </summary>
        /// <param name="outTaskCompleteDto">请求参数</param>
        /// <returns></returns>
        public Task<WebResponseContent> OutTaskComplete(OutTaskCompleteDto outTaskCompleteDto);
        /// <summary>
        /// ä»»åŠ¡å®ŒæˆæŽ¥å£
        /// </summary>
        /// <param name="wCSTask"></param>
        /// <returns></returns>
        //public Task<WebResponseContent> TaskCompleted(WCSTaskDTO wCSTask);
        /// <summary>
        /// ä»»åŠ¡å–æ¶ˆ
        /// </summary>
        /// <param name="taskCancelDto">请求参数</param>
        /// <returns></returns>
        public Task<AGVResponse> TaskCancelAsync(TaskCancelDto taskCancelDto);
        /// <summary>
        /// å–放货完成
        /// </summary>
        /// <param name="taskCompleteDto">请求参数</param>
        /// <returns></returns>
        public Task<AGVResponse> TaskCompleteAsync(TaskCompleteDto taskCompleteDto);
        /// <summary>
        /// è¾“送线申请进入
        /// </summary>
        /// <param name="applyEnterDto">请求参数</param>
        /// <returns></returns>
        public Task<AGVResponse> ApplyEnterAsync(ApplyEnterDto applyEnterDto);
        #endregion æžå·åº“任务模块
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITask_HtyService.cs
@@ -6,6 +6,7 @@
using System.Text;
using System.Threading.Tasks;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_Core.Enums;
using WIDESEA_DTO.Stock;
@@ -15,4 +16,8 @@
public interface ITask_HtyService : IService<Dt_Task_Hty>
{
    /// <summary>
    /// èŽ·å–ä»»åŠ¡åŽ†å²ä»“å‚¨æŽ¥å£
    /// </summary>
    IRepository<Dt_Task_Hty> Repository { get; }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
@@ -1,6 +1,8 @@
using WIDESEA_Common.StockEnum;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_DTO.Stock;
using WIDESEA_IBasicService;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
@@ -17,11 +19,23 @@
        public IRepository<Dt_StockInfo> Repository => BaseDal;
        /// <summary>
        /// è´§ä½ä¿¡æ¯æœåŠ¡æŽ¥å£ï¼ˆç”¨äºŽèŽ·å–ä»“åº“è´§ä½ä¿¡æ¯ï¼‰
        /// </summary>
        private readonly ILocationInfoService _locationInfoService;
        /// <summary>
        /// ä»“库信息服务接口(用于获取仓库基本信息)
        /// </summary>
        private readonly IWarehouseService _warehouseService;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="baseDal">基础数据访问对象</param>
        public StockInfoService(IRepository<Dt_StockInfo> baseDal) : base(baseDal)
        public StockInfoService(IRepository<Dt_StockInfo> baseDal, ILocationInfoService locationInfoService, IWarehouseService warehouseService) : base(baseDal)
        {
            _locationInfoService = locationInfoService;
            _warehouseService = warehouseService;
        }
        /// <summary>
@@ -78,5 +92,128 @@
        {
            return await BaseDal.QueryFirstAsync(x => x.PalletCode == palletCode && x.LocationCode == locationCode);
        }
        /// <summary>
        /// èŽ·å–ä»“åº“3D布局数据
        /// </summary>
        /// <param name="warehouseId">仓库ID</param>
        /// <returns>3D布局DTO</returns>
        public async Task<Stock3DLayoutDTO> Get3DLayoutAsync(int warehouseId)
        {
            // 1. æŸ¥è¯¢ä»“库信息
            var warehouse = await _warehouseService.Repository.QueryFirstAsync(x => x.WarehouseId == warehouseId);
            // 2. æŸ¥è¯¢è¯¥ä»“库所有货位
            var locations = await _locationInfoService.Repository.QueryDataAsync(x => x.WarehouseId == warehouseId);
            // 3. æŸ¥è¯¢è¯¥ä»“库所有库存信息(包含Details导航属性)
            var stockInfos = await Repository.QueryDataNavAsync(x => x.WarehouseId == warehouseId && x.LocationId != 0);
            // 4. æå–物料编号和批次号列表(去重)
            var materielCodeList = stockInfos
                .Where(s => s.Details != null)
                .SelectMany(s => s.Details)
                .Select(d => d.MaterielCode)
                .Where(c => !string.IsNullOrEmpty(c))
                .Distinct()
                .ToList();
            var batchNoList = stockInfos
                .Where(s => s.Details != null)
                .SelectMany(s => s.Details)
                .Select(d => d.BatchNo)
                .Where(b => !string.IsNullOrEmpty(b))
                .Distinct()
                .ToList();
            // 5. åˆ›å»ºåº“存字典用于快速查找(以LocationId为键)
            var stockDict = stockInfos.ToDictionary(s => s.LocationId, s => s);
            // 6. æ˜ å°„每个货位到Location3DItemDTO
            const float defaultMaxCapacity = 100f;
            var locationItems = locations.Select(loc =>
            {
                var item = new Location3DItemDTO
                {
                    LocationId = loc.Id,
                    LocationCode = loc.LocationCode,
                    Row = loc.Row,
                    Column = loc.Column,
                    Layer = loc.Layer,
                    LocationStatus = loc.LocationStatus,
                    MaxCapacity = defaultMaxCapacity
                };
                // å°è¯•从库存字典中获取库存信息
                if (stockDict.TryGetValue(loc.Id, out var stockInfo))
                {
                    // ç©ºæ‰˜ç›˜ä¹Ÿæœ‰åº“存记录,只是不包含明细
                    item.PalletCode = stockInfo.PalletCode;
                    item.StockStatus = stockInfo.StockStatus; // ç›´æŽ¥ä½¿ç”¨åŽç«¯åº“存状态
                    // åªæœ‰å½“Details不为null且有数据时才处理库存明细
                    if (stockInfo.Details != null && stockInfo.Details.Any())
                    {
                        item.StockQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
                        // èŽ·å–ç¬¬ä¸€ä¸ªæ˜Žç»†çš„ç‰©æ–™ä¿¡æ¯ï¼ˆå¦‚æžœå­˜åœ¨ï¼‰
                        var firstDetail = stockInfo.Details.FirstOrDefault();
                        if (firstDetail != null)
                        {
                            item.MaterielCode = firstDetail.MaterielCode;
                            item.MaterielName = firstDetail.MaterielName;
                            item.BatchNo = firstDetail.BatchNo;
                        }
                        // å¡«å……库存明细列表
                        item.Details = stockInfo.Details.Select(d => new StockDetailItemDTO
                        {
                            Id = d.Id,
                            MaterielCode = d.MaterielCode,
                            MaterielName = d.MaterielName,
                            BatchNo = d.BatchNo,
                            StockQuantity = d.StockQuantity,
                            Unit = d.Unit,
                            ProductionDate = d.ProductionDate,
                            EffectiveDate = d.EffectiveDate,
                            OrderNo = d.OrderNo,
                            Status = d.Status
                        }).ToList();
                    }
                    else
                    {
                        // ç©ºæ‰˜ç›˜ï¼ˆæ— æ˜Žç»†ï¼‰
                        item.StockQuantity = 0;
                        item.Details = new List<StockDetailItemDTO>(); // ç¡®ä¿æ˜¯ç©ºåˆ—表而非null
                    }
                }
                else
                {
                    // æ— åº“存记录,货位为空
                    item.StockStatus = 0; // ç©ºé—²
                    item.StockQuantity = 0;
                }
                return item;
            }).ToList();
            // 7. è®¡ç®—仓库尺寸
            var maxRow = locations.Any() ? locations.Max(l => l.Row) : 0;
            var maxColumn = locations.Any() ? locations.Max(l => l.Column) : 0;
            var maxLayer = locations.Any() ? locations.Max(l => l.Layer) : 0;
            // 8. æž„建返回结果
            return new Stock3DLayoutDTO
            {
                WarehouseId = warehouseId,
                WarehouseName = warehouse?.WarehouseName ?? string.Empty,
                MaxRow = maxRow,
                MaxColumn = maxColumn,
                MaxLayer = maxLayer,
                MaterielCodeList = materielCodeList,
                BatchNoList = batchNoList,
                Locations = locationItems
            };
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs
@@ -1,7 +1,9 @@
using SqlSugar;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_DTO.MES;
using WIDESEA_DTO.Stock;
using WIDESEA_IBasicService;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
@@ -33,6 +35,11 @@
        public IStockInfo_HtyService StockInfo_HtyService { get; }
        /// <summary>
        /// Mes接口服务
        /// </summary>
        public IMesService _mesService { get; }
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="stockInfoDetailService">库存明细服务</param>
@@ -43,12 +50,14 @@
            IStockInfoDetailService stockInfoDetailService,
            IStockInfoService stockInfoService,
            IStockInfoDetail_HtyService stockInfoDetail_HtyService,
            IStockInfo_HtyService stockInfo_HtyService)
            IStockInfo_HtyService stockInfo_HtyService,
            IMesService mesService)
        {
            StockInfoDetailService = stockInfoDetailService;
            StockInfoService = stockInfoService;
            StockInfoDetail_HtyService = stockInfoDetail_HtyService;
            StockInfo_HtyService = stockInfo_HtyService;
            _mesService = mesService;
        }
        /// <summary>
@@ -120,6 +129,20 @@
                    Status = StockStatusEmun.组盘暂存.GetHashCode(),
                }).ToList();
                var bindRequest = new BindContainerRequest
                {
                    ContainerCode = stock?.TargetPalletNo,
                    EquipmentCode = "STK-GROUP-001",
                    ResourceCode = "STK-GROUP-001",
                    LocalTime = now,
                    OperationType = 0, // 0代表组盘
                    ContainerSfcList = details.Select(d => new ContainerSfcItem
                    {
                        Sfc = d.SerialNumber,
                        Location = d.InboundOrderRowNo.ToString(),
                    }).ToList()
                };
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    var existingStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
@@ -140,9 +163,15 @@
                        Creater = "system",
                        Details = details
                    };
                    result = StockInfoService.Repository.AddData(entity, x => x.Details);
                    return result ? content.OK("组盘成功") : content.Error("组盘失败");
                    if (!result) return content.Error("组盘失败");
                    var mesResult = _mesService.BindContainer(bindRequest);
                    if (mesResult == null || mesResult.Data == null || !mesResult.Data.IsSuccess)
                    {
                        return content.Error($"组盘成功,但MES绑定失败: {mesResult?.Data?.Msg ?? mesResult?.ErrorMessage ?? "未知错误"}");
                    }
                    return content.OK("组盘成功");
                });
            }
            catch (Exception ex)
@@ -203,9 +232,45 @@
                    if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock, targetStock }, "换盘")) <= 0)
                        return content.Error("换盘历史记录保存失败");
                    // è°ƒç”¨MES解绑源托盘电芯
                    var unbindRequest = new UnBindContainerRequest
                    {
                        EquipmentCode = "STK-GROUP-001",
                        ResourceCode = "STK-GROUP-001",
                        LocalTime = DateTime.Now,
                        ContainCode = stock.SourcePalletNo,
                        SfcList = detailEntities.Select(d => d.SerialNumber).ToList()
                    };
                    var unbindResult = _mesService.UnBindContainer(unbindRequest);
                    if (unbindResult == null || unbindResult.Data == null || !unbindResult.Data.IsSuccess)
                    {
                        return content.Error($"换盘成功,但MES解绑失败: {unbindResult?.Data?.Msg ?? unbindResult?.ErrorMessage ?? "未知错误"}");
                    }
                    detailEntities.ForEach(d => d.StockId = targetStock.Id);
                    var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
                    if (!result) return content.Error("换盘失败");
                    // è°ƒç”¨MES绑定目标托盘电芯
                    var bindRequest = new BindContainerRequest
                    {
                        ContainerCode = stock.TargetPalletNo,
                        EquipmentCode = "STK-GROUP-001",
                        ResourceCode = "STK-GROUP-001",
                        LocalTime = DateTime.Now,
                        OperationType = 0,
                        ContainerSfcList = detailEntities.Select(d => new ContainerSfcItem
                        {
                            Sfc = d.SerialNumber,
                            Location = d.InboundOrderRowNo.ToString()
                        }).ToList()
                    };
                    var bindResult = _mesService.BindContainer(bindRequest);
                    if (bindResult == null || bindResult.Data == null || !bindResult.Data.IsSuccess)
                    {
                        return content.Error($"换盘成功,但MES绑定失败: {bindResult?.Data?.Msg ?? bindResult?.ErrorMessage ?? "未知错误"}");
                    }
                    return content.OK("换盘成功");
                });
            }
@@ -250,6 +315,21 @@
                    if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock }, "拆盘")) <= 0)
                        return content.Error("拆盘历史记录保存失败");
                    // è°ƒç”¨MES解绑电芯
                    var unbindRequest = new UnBindContainerRequest
                    {
                        EquipmentCode = "STK-GROUP-001",
                        ResourceCode = "STK-GROUP-001",
                        LocalTime = DateTime.Now,
                        ContainCode = stock.SourcePalletNo,
                        SfcList = detailEntities.Select(d => d.SerialNumber).ToList()
                    };
                    var unbindResult = _mesService.UnBindContainer(unbindRequest);
                    if (unbindResult == null || unbindResult.Data == null || !unbindResult.Data.IsSuccess)
                    {
                        return content.Error($"拆盘成功,但MES解绑失败: {unbindResult?.Data?.Msg ?? unbindResult?.ErrorMessage ?? "未知错误"}");
                    }
                    var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
                    if (!result) return content.Error("拆盘失败");
                    return content.OK("拆盘成功");
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/WIDESEA_StockService.csproj
@@ -7,6 +7,7 @@
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\WIDESEA_IBasicService\WIDESEA_IBasicService.csproj" />
    <ProjectReference Include="..\WIDESEA_IRecordService\WIDESEA_IRecordService.csproj" />
    <ProjectReference Include="..\WIDESEA_IStockService\WIDESEA_IStockService.csproj" />
  </ItemGroup>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -2,16 +2,20 @@
using MapsterMapper;
using Microsoft.Extensions.Configuration;
using SqlSugar;
using System.DirectoryServices.Protocols;
using System.Text.Json;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_Core.Core;
using WIDESEA_Core.Enums;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.GradingMachine;
using WIDESEA_DTO.MES;
using WIDESEA_DTO.Stock;
using WIDESEA_DTO.Task;
using WIDESEA_IBasicService;
@@ -29,6 +33,10 @@
        private readonly HttpClientHelper _httpClientHelper;
        private readonly IConfiguration _configuration;
        private readonly RoundRobinService _roundRobinService;
        private readonly IMesService _mesService;
        private readonly ITask_HtyService _task_HtyService;
        private readonly IStockInfo_HtyService _stockInfo_HtyService;
        private readonly IUnitOfWorkManage _unitOfWorkManage;
        public IRepository<Dt_Task> Repository => BaseDal;
@@ -48,7 +56,11 @@
            ILocationInfoService locationInfoService,
            HttpClientHelper httpClientHelper,
            IConfiguration configuration,
            RoundRobinService roundRobinService) : base(BaseDal)
            RoundRobinService roundRobinService,
            IMesService mesService,
            ITask_HtyService task_HtyService,
            IStockInfo_HtyService stockInfo_HtyService,
            IUnitOfWorkManage unitOfWorkManage) : base(BaseDal)
        {
            _mapper = mapper;
            _stockInfoService = stockInfoService;
@@ -56,6 +68,10 @@
            _httpClientHelper = httpClientHelper;
            _configuration = configuration;
            _roundRobinService = roundRobinService;
            _mesService = mesService;
            _task_HtyService = task_HtyService;
            _stockInfo_HtyService = stockInfo_HtyService;
            _unitOfWorkManage = unitOfWorkManage;
        }
        #region WCS逻辑处理
@@ -212,8 +228,15 @@
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                // åˆ¤æ–­æ˜¯ä¸æ˜¯æžå·åº“任务
                //if (taskDto.WarehouseId == (int)WarehouseEnum.FJ1 || taskDto.WarehouseId == (int)WarehouseEnum.ZJ1)
                //{
                //    return WebResponseContent.Instance.OK();
                //}
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    WebResponseContent content = new WebResponseContent();
                    stockInfo.LocationCode = location.LocationCode;
                    stockInfo.LocationId = location.Id;
                    stockInfo.OutboundDate = task.Roadway switch
@@ -230,7 +253,20 @@
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    return await CompleteTaskAsync(task);
                    // è°ƒç”¨MES托盘进站
                    var inboundRequest = new InboundInContainerRequest
                    {
                        EquipmentCode = "STK-GROUP-001",
                        ResourceCode = "STK-GROUP-001",
                        LocalTime = DateTime.Now,
                        ContainerCode = taskDto.PalletCode
                    };
                    var inboundResult = _mesService.InboundInContainer(inboundRequest);
                    if (inboundResult == null || inboundResult.Data == null || !inboundResult.Data.IsSuccess)
                    {
                        return content.Error($"任务完成失败:MES进站失败: {inboundResult?.Data?.Msg ?? inboundResult?.ErrorMessage ?? "未知错误"}");
                    }
                    return await CompleteTaskAsync(task, "入库完成");
                });
            }
            catch (Exception ex)
@@ -255,6 +291,19 @@
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                // åˆ¤æ–­æ˜¯ä¸æ˜¯æžå·åº“任务
                if (taskDto.WarehouseId == (int)WarehouseEnum.FJ1 || taskDto.WarehouseId == (int)WarehouseEnum.ZJ1)
                {
                    OutTaskCompleteDto outTaskCompleteDto = new OutTaskCompleteDto()
                    {
                        TaskId = task.OrderNo,
                        DevId = task.TargetAddress,
                        ReqTime = DateTime.Now.ToString()
                    };
                    return await OutTaskComplete(outTaskCompleteDto);
                }
                WebResponseContent content = new WebResponseContent();
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    stockInfo.LocationId = 0;
@@ -267,7 +316,23 @@
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    return await CompleteTaskAsync(task);
                    // è°ƒç”¨MES托盘出站
                    var outboundRequest = new OutboundInContainerRequest
                    {
                        EquipmentCode = "STK-GROUP-001",
                        ResourceCode = "STK-GROUP-001",
                        LocalTime = DateTime.Now,
                        ContainerCode = taskDto.PalletCode,
                        ParamList = new List<ParamItem>()
                    };
                    var outboundResult = _mesService.OutboundInContainer(outboundRequest);
                    if (outboundResult == null || outboundResult.Data == null || !outboundResult.Data.IsSuccess)
                    {
                        return content.Error($"任务完成失败:MES出站失败: {outboundResult?.Data?.Msg ?? outboundResult?.ErrorMessage ?? "未知错误"}");
                    }
                    return await CompleteTaskAsync(task, "出库完成");
                });
            }
            catch (Exception ex)
@@ -313,7 +378,7 @@
                    if (!updateSourceResult || !updateTargetResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("移库任务完成失败");
                    return await CompleteTaskAsync(task);
                    return await CompleteTaskAsync(task, "移库完成");
                });
            }
            catch (Exception ex)
@@ -350,6 +415,61 @@
        }
        /// <summary>
        /// ç©ºæ‰˜ç›˜å…¥åº“完成
        /// </summary>
        public async Task<WebResponseContent> InboundFinishTaskTrayAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
                var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.TargetAddress);
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    stockInfo.LocationCode = location.LocationCode;
                    stockInfo.LocationId = location.Id;
                    stockInfo.StockStatus = StockStatusEmun.空托盘库存.GetHashCode();
                    location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    // ä¿å­˜ä»»åŠ¡åŽ†å²
                    var historyTask = _mapper.Map<Dt_Task_Hty>(task);
                    historyTask.InsertTime = DateTime.Now;
                    historyTask.OperateType = "空托盘入库完成";
                    if (await _task_HtyService.Repository.AddDataAsync(historyTask) <= 0)
                        return WebResponseContent.Instance.Error("任务历史保存失败");
                    // ä¿å­˜åº“存历史
                    var historyStock = _mapper.Map<Dt_StockInfo_Hty>(stockInfo);
                    historyStock.InsertTime = DateTime.Now;
                    historyStock.OperateType = "空托盘入库完成";
                    if (await _stockInfo_HtyService.Repository.AddDataAsync(historyStock) <= 0)
                        return WebResponseContent.Instance.Error("库存历史保存失败");
                    var deleteResult = await BaseDal.DeleteDataAsync(task);
                    if (!deleteResult) return WebResponseContent.Instance.Error("任务完成失败");
                    return WebResponseContent.Instance.OK("任务完成");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
            }
        }
        /// <summary>
        /// åˆ›å»ºç©ºæ‰˜ç›˜å‡ºåº“任务
        /// </summary>
        /// <param name="taskDto"></param>
@@ -358,7 +478,6 @@
        {
            try
            {
                var stockInfo = await _stockInfoService.Repository.QueryDataNavFirstAsync(x => x.LocationDetails.WarehouseId == taskDto.WarehouseId && x.LocationDetails.LocationStatus == LocationStatusEnum.InStock.GetHashCode() && x.StockStatus == StockStatusEmun.空托盘库存.GetHashCode());
                if (stockInfo == null)
                    return WebResponseContent.Instance.Error("未找到对应的库存信息");
@@ -393,6 +512,61 @@
        }
        /// <summary>
        /// ç©ºæ‰˜ç›˜å‡ºåº“完成
        /// </summary>
        public async Task<WebResponseContent> OutboundFinishTaskTrayAsync(CreateTaskDto taskDto)
        {
            try
            {
                var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
                if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
                var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.SourceAddress);
                if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
                var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
                if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    stockInfo.LocationId = 0;
                    stockInfo.LocationCode = null;
                    stockInfo.StockStatus = StockStatusEmun.出库完成.GetHashCode();
                    location.LocationStatus = LocationStatusEnum.Free.GetHashCode();
                    var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                    var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
                    if (!updateLocationResult || !updateStockResult)
                        return WebResponseContent.Instance.Error("任务完成失败");
                    // ä¿å­˜ä»»åŠ¡åŽ†å²
                    var historyTask = _mapper.Map<Dt_Task_Hty>(task);
                    historyTask.InsertTime = DateTime.Now;
                    historyTask.OperateType = "空托盘出库完成";
                    if (await _task_HtyService.Repository.AddDataAsync(historyTask) <= 0)
                        return WebResponseContent.Instance.Error("任务历史保存失败");
                    // ä¿å­˜åº“存历史
                    var historyStock = _mapper.Map<Dt_StockInfo_Hty>(stockInfo);
                    historyStock.InsertTime = DateTime.Now;
                    historyStock.OperateType = "空托盘出库完成";
                    if (await _stockInfo_HtyService.Repository.AddDataAsync(historyStock) <= 0)
                        return WebResponseContent.Instance.Error("库存历史保存失败");
                    var deleteResult = await BaseDal.DeleteDataAsync(task);
                    if (!deleteResult) return WebResponseContent.Instance.Error("任务完成失败");
                    return WebResponseContent.Instance.OK("任务完成");
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
            }
        }
        /// <summary>
        /// ä¿®æ”¹ä»»åŠ¡çŠ¶æ€ï¼ˆæ ¹æ®ä»»åŠ¡ID修改为指定状态)
        /// </summary>
        /// <param name="taskId"></param>
@@ -412,11 +586,10 @@
                return WebResponseContent.Instance.OK("修改成功", tasks);
            }
            catch (Exception ex)
            {
            {
                return WebResponseContent.Instance.Error($"修改失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æŸ¥æ‰¾æ‰˜ç›˜æ˜¯å¦æœ‰ä»»åŠ¡
@@ -442,14 +615,16 @@
        /// <summary>
        /// å®Œæˆä»»åŠ¡åŽç»Ÿä¸€å¤„ç†ï¼ˆåˆ é™¤ä»»åŠ¡æ•°æ®ï¼‰
        /// </summary>
        private async Task<WebResponseContent> CompleteTaskAsync(Dt_Task task)
        private async Task<WebResponseContent> CompleteTaskAsync(Dt_Task task, string operateType = "")
        {
            var deleteTaskResult = await BaseDal.DeleteDataAsync(task);
            if (!deleteTaskResult) return WebResponseContent.Instance.Error("任务完成失败");
            // ä¿ç•™åŽ†å²å¯¹è±¡æž„å»ºé€»è¾‘ï¼ŒåŽç»­å¯æŽ¥å…¥åŽ†å²è¡¨è½åº“
            var historyTask = _mapper.Map<Dt_Task_Hty>(task);
            historyTask.InsertTime = DateTime.Now;
            historyTask.OperateType = operateType;
            var saveResult = await _task_HtyService.Repository.AddDataAsync(historyTask) > 0;
            if (!saveResult) return WebResponseContent.Instance.Error("任务历史保存失败");
            return WebResponseContent.Instance.OK("任务完成");
        }
@@ -943,5 +1118,406 @@
        }
        #endregion åˆ†å®¹æŸœæŽ¥å£
        #region æžå·åº“任务模块
        public string AGV_OutTaskComplete = WIDESEA_Core.Helper.AppSettings.Configuration["AGV_OutTaskComplete"]; // ä¸ŠæŠ¥AGV出库输送线完成
        public string WCS_ReceiveTask = WIDESEA_Core.Helper.AppSettings.Configuration["WCS_ReceiveTask"]; // WMS输送线任务下发
        /// <summary>
        /// å‡ºå…¥åº“申请
        /// </summary>
        /// <param name="applyInOutDto">请求参数</param>
        /// <returns></returns>
        public async Task<AGVResponse> ApplyInOutAsync(ApplyInOutDto applyInOutDto)
        {
            AGVResponse aGVResponse = new AGVResponse();
            try
            {
                // å‚数验证
                if (applyInOutDto == null) return aGVResponse.Error("请求参数不能为空");
                if (string.IsNullOrWhiteSpace(applyInOutDto.TrayNumber)) return aGVResponse.Error("托盘号不能为空");
                if (string.IsNullOrWhiteSpace(applyInOutDto.TaskId)) return aGVResponse.Error("任务号不能为空");
                if (string.IsNullOrWhiteSpace(applyInOutDto.MaterialType)) return aGVResponse.Error("物料类型不能为空");
                if (string.IsNullOrWhiteSpace(applyInOutDto.MaterialName)) return aGVResponse.Error("物料描述不能为空");
                if (string.IsNullOrWhiteSpace(applyInOutDto.ReqTime)) return aGVResponse.Error("请求时间不能为空");
                if (applyInOutDto.Floor != 1 && applyInOutDto.Floor != 2) return aGVResponse.Error($"楼层段错误,必须为1(模切段)或2(卷绕段),当前值:{applyInOutDto.Floor}");
                if (applyInOutDto.YinYang != 1 && applyInOutDto.YinYang != 2) return aGVResponse.Error($"阴阳极错误,必须为1(阴极)或2(阳极),当前值:{applyInOutDto.YinYang}");
                if (applyInOutDto.InOut != 1 && applyInOutDto.InOut != 2) return aGVResponse.Error($"出入库类型错误,必须为1(入库)或2(出库),当前值:{applyInOutDto.InOut}");
                if (applyInOutDto.InOut == 1) // å…¥åº“
                {
                    if (applyInOutDto.Width == null || applyInOutDto.Width <= 0) return aGVResponse.Error("入库时宽度不能为空且必须大于0");
                    if (string.IsNullOrWhiteSpace(applyInOutDto.Group)) return aGVResponse.Error("入库时整托组别不能为空");
                }
                // æ£€æŸ¥ä»»åŠ¡æ˜¯å¦å·²å­˜åœ¨
                var existingTask = await BaseDal.QueryFirstAsync(x => x.PalletCode == applyInOutDto.TrayNumber);
                if (existingTask != null) return aGVResponse.Error($"WMS已有当前任务,不可重复下发,任务号:{applyInOutDto.TaskId}");
                // åˆ›å»ºä»»åŠ¡
                Dt_Task task = new Dt_Task
                {
                    OrderNo = applyInOutDto.TaskId, // ä½¿ç”¨AGV的任务号
                    PalletCode = applyInOutDto.TrayNumber,
                    PalletType = applyInOutDto.Floor,
                    //WarehouseId = applyInOutDto.YinYang,
                    Grade = 1,
                    Creater = "AGV",
                    CreateDate = DateTime.Now,
                    Remark = $"物料类型:{applyInOutDto.MaterialType},物料描述:{applyInOutDto.MaterialName}"
                };
                // æ ¹æ®é˜´æž/阳极设置巷道和站台
                if (applyInOutDto.YinYang == 1) // é˜´æž
                {
                    // è´§ç‰©åˆ°è¾¾åŽåˆ†é…è´§ç‰©ä½
                    task.Roadway = WarehouseEnum.FJ1.ToString();
                    task.WarehouseId = (int)WarehouseEnum.FJ1;
                    task.SourceAddress = applyInOutDto.InOut == 1 ? "D10010" : "D10020"; // å…¥åº“口/出库口
                    task.NextAddress = "D10080"; // é˜´æžå…¬å…±å‡ºåº“口
                    task.TargetAddress = "阴极卷库";
                }
                else if (applyInOutDto.YinYang == 2) // é˜³æž
                {
                    task.Roadway = WarehouseEnum.ZJ1.ToString();
                    task.WarehouseId = (int)WarehouseEnum.ZJ1;
                    task.SourceAddress = applyInOutDto.InOut == 1 ? "D10100" : "D10090"; // å…¥åº“口/出库口
                    task.NextAddress = "D10160"; // é˜³æžå…¬å…±å‡ºåº“口
                    task.TargetAddress = "正极卷库";
                }
                // æ ¹æ®å‡ºå…¥åº“类型设置目标地址
                if (applyInOutDto.InOut == 1) // å…¥åº“
                {
                    var stockInfo = await _stockInfoService.GetStockInfoAsync(applyInOutDto.TrayNumber);
                    if (stockInfo != null) return aGVResponse.Error($"当前托盘{applyInOutDto.TrayNumber}已经入库了");
                    task.TaskType = (int)TaskInboundTypeEnum.Inbound;
                    task.TaskStatus = (int)TaskInStatusEnum.InNew;
                    task.CurrentAddress = task.SourceAddress; // å½“前在入库口
                    // ä¿å­˜ä»»åŠ¡
                    var result = await BaseDal.AddDataAsync(task);
                }
                else // å‡ºåº“
                {
                    // åˆ¤æ–­æ˜¯å¦ç§»åº“
                    task.TaskType = (int)TaskOutboundTypeEnum.Outbound;
                    task.TaskStatus = (int)TaskOutStatusEnum.OutNew;
                    task.CurrentAddress = task.SourceAddress; // å½“前在入库口
                    // æŸ¥æ‰¾åº“å­˜
                    var stockInfo = await _stockInfoService.GetStockInfoAsync(applyInOutDto.TrayNumber);
                    if (stockInfo == null) return aGVResponse.Error($"未找到托盘{applyInOutDto.TrayNumber}的库存信息");
                    // éªŒè¯åº“存所属是否正确
                    if (stockInfo.WarehouseId != applyInOutDto.YinYang) return aGVResponse.Error($"托盘{applyInOutDto.TrayNumber}不属于当前{(applyInOutDto.YinYang == 1 ? "阴极" : "阳极")}");
                    if (stockInfo.StockStatus != (int)StockStatusEmun.入库完成) return aGVResponse.Error($"托盘{applyInOutDto.TrayNumber}正在移动中,请稍后!");
                    task.SourceAddress = stockInfo.LocationCode; // æºåœ°å€ä¸ºè´§ä½
                    task.CurrentAddress = stockInfo.LocationCode; // å½“前位置在货位
                    task.TargetAddress = applyInOutDto.YinYang == 1 ? "D10020" : "D10090"; // ç›®æ ‡åœ°å€ä¸ºå‡ºåº“口
                    // ä¿å­˜ä»»åŠ¡
                    var result = await BaseDal.AddDataAsync(task);
                    // åˆ¤æ–­æ˜¯å¦ç§»åº“
                    //var transferTask = await _locationInfoService.TransferCheckAsync(result);
                }
                // æž„建响应数据
                AGVDataDto aGVDataDto = new AGVDataDto
                {
                    DevId = applyInOutDto.InOut == 1 ? task.SourceAddress : task.TargetAddress,
                    TrayNumber = task.PalletCode,
                    Group = applyInOutDto.Group,
                    Width = applyInOutDto.Width ?? 0,
                    LabelNumber = applyInOutDto.LabelNumber,
                    ProductNo = applyInOutDto.ProductNo,
                    ProductName = applyInOutDto.ProductName,
                    Quantity = applyInOutDto.Quantity,
                    UomCode = applyInOutDto.UomCode,
                    ProductType = applyInOutDto.ProductType,
                    Equipment = applyInOutDto.Equipment,
                    ProductionDate = applyInOutDto.ProductionDate,
                    LowerLimitTime = applyInOutDto.LowerLimitTime,
                    WarningTime = applyInOutDto.WarningTime,
                    OverdueTime = applyInOutDto.OverdueTime
                };
                return aGVResponse.OK(aGVDataDto);
            }
            catch (Exception ex)
            {
                return aGVResponse.Error($"WMS任务创建接口错误: {ex.Message}");
            }
        }
        /// <summary>
        /// æ‰‹åŠ¨å‡ºåº“å®Œæˆåé¦ˆç»™AGV
        /// </summary>
        /// <param name="outTaskCompleteDto">请求参数</param>
        /// <returns></returns>
        public async Task<WebResponseContent> OutTaskComplete(OutTaskCompleteDto outTaskCompleteDto)
        {
            WebResponseContent webResponse = new WebResponseContent();
            try
            {
                if (outTaskCompleteDto == null) return webResponse.Error("请求参数不能为空");
                if (string.IsNullOrWhiteSpace(outTaskCompleteDto.TaskId)) return webResponse.Error("任务号不能为空");
                if (string.IsNullOrWhiteSpace(outTaskCompleteDto.DevId)) return webResponse.Error("出库口编号不能为空");
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == outTaskCompleteDto.TaskId);
                if (task == null) return webResponse.Error("未找到任务信息");
                outTaskCompleteDto.ReqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                var httpResponse = _httpClientHelper.Post<AGVResponse>(AGV_OutTaskComplete, outTaskCompleteDto.ToJson()).Data;
                // åˆ¤æ–­è¿œç¨‹æŽ¥å£è¿”回是否成功
                if (httpResponse != null && httpResponse.Data != null)
                {
                    AGVResponse agvResponse = httpResponse;
                    // æ ¹æ®code字段判断,code为true表示成功
                    if (agvResponse.Code == true)
                    {
                        var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
                        if (stockInfo == null) return webResponse.Error($"未找到托盘{task.PalletCode}的库存信息");
                        var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
                        if (locationInfo == null) return webResponse.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
                        if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                        {
                            return webResponse.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
                        }
                        locationInfo.LocationStatus = (int)LocationStatusEnum.Free;
                        _unitOfWorkManage.BeginTran();
                        BaseDal.DeleteData(task);
                        _locationInfoService.UpdateData(locationInfo);
                        _stockInfoService.DeleteData(stockInfo);
                        _unitOfWorkManage.CommitTran();
                        return webResponse.OK(agvResponse.Msg);
                    }
                    else
                    {
                        // å¤±è´¥ï¼šè¿”回AGV返回的错误信息
                        return webResponse.Error(string.IsNullOrWhiteSpace(agvResponse.Msg)
                            ? "AGV接口调用失败"
                            : agvResponse.Msg);
                    }
                }
                else
                {
                    // HTTP请求本身失败
                    return webResponse.Error(httpResponse?.Msg ?? "AGV接口调用异常");
                }
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return webResponse.Error($"WMS任务完成接口错误:{ex.Message}");
            }
        }
        ///// <summary>
        ///// ä»»åŠ¡å®ŒæˆæŽ¥å£
        ///// </summary>
        ///// <param name="wCSTask"></param>
        ///// <returns></returns>
        //public async Task<WebResponseContent> TaskCompleted(WCSTaskDTO wCSTask)
        //{
        //    WebResponseContent webResponse = new WebResponseContent();
        //    try
        //    {
        //        Dt_Task task = await BaseDal.QueryFirstAsync(x => x.TaskId == wCSTask.TaskNum && x.PalletCode == wCSTask.PalletCode);
        //        if (task == null) return webResponse.Error("未找到任务信息");
        //        // å‡ºåº“完成反馈
        //        OutTaskCompleteDto outTaskCompleteDto = new OutTaskCompleteDto()
        //        {
        //            TaskId = task.OrderNo,
        //            DevId = task.TargetAddress,
        //            ReqTime = DateTime.Now.ToString()
        //        };
        //        return await OutTaskComplete(outTaskCompleteDto);
        //    }
        //    catch (Exception ex)
        //    {
        //        return webResponse.Error($"WMS任务完成接口错误:{ex.Message}");
        //    }
        //}
        /// <summary>
        /// è¾“送线申请进入
        /// </summary>
        /// <param name="applyEnterDto">请求参数</param>
        /// <returns></returns>
        public async Task<AGVResponse> ApplyEnterAsync(ApplyEnterDto applyEnterDto)
        {
            AGVResponse aGVResponse = new AGVResponse();
            try
            {
                // å‚数验证
                if (applyEnterDto == null) return aGVResponse.Error("请求参数不能为空");
                if (string.IsNullOrWhiteSpace(applyEnterDto.DevId)) return aGVResponse.Error("设备编号不能为空");
                if (string.IsNullOrWhiteSpace(applyEnterDto.TaskId)) return aGVResponse.Error("任务号不能为空");
                if (applyEnterDto.InOut != 1 && applyEnterDto.InOut != 2) return aGVResponse.Error($"出入库类型错误,必须为1(入库)或2(出库),当前值:{applyEnterDto.InOut}");
                // æŸ¥è¯¢ä»»åŠ¡æ˜¯å¦å­˜åœ¨
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == applyEnterDto.TaskId);
                if (task == null) return aGVResponse.Error($"未找到任务信息,任务号:{applyEnterDto.TaskId}");
                if (applyEnterDto.InOut == 1 && task.TaskType == (int)TaskInboundTypeEnum.Inbound && task.TaskStatus == (int)TaskInStatusEnum.InNew)
                {
                    aGVResponse.OK();
                }
                else if (applyEnterDto.InOut == 2 && task.TaskType == (int)TaskOutboundTypeEnum.Outbound && task.TaskStatus == (int)TaskStatusEnum.Line_Finish)
                {
                    aGVResponse.OK();
                }
                return aGVResponse.Error($"输送线{applyEnterDto.DevId}当前繁忙,请稍后重试");
                // Dt_Task.SourceAddress询问wcs站台1-阴极入口叫D10010和D10020;2-阳极D10090和D10100是否有货物
                //var httpResponse = _httpClientHelper.Post<WebResponseContent>("http://127.0.0.1:9999/api/Task/ApplyInOut", JsonSerializer.Serialize(task)).Data;
                //if (httpResponse != null && httpResponse.Status == true)
                //{
                //    _unitOfWorkManage.BeginTran();
                //    task.TaskStatus = (int)TaskStatusEnum.AGV_Executing;
                //    await BaseDal.UpdateDataAsync(task);
                //    _unitOfWorkManage.CommitTran();
                //    return aGVResponse.OK();
                //}
                //else
                //{
                //    return aGVResponse.Error($"站台{task.SourceAddress}已经载货");
                //}
            }
            catch (Exception ex)
            {
                //_unitOfWorkManage.RollbackTran();
                return aGVResponse.Error($"WMS输送线申请接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// å–放货完成
        /// </summary>
        /// <param name="taskCompleteDto">请求参数</param>
        /// <returns></returns>
        public async Task<AGVResponse> TaskCompleteAsync(TaskCompleteDto taskCompleteDto)
        {
            AGVResponse aGVResponse = new AGVResponse();
            try
            {
                if (taskCompleteDto == null) return aGVResponse.Error("请求参数不能为空");
                if (string.IsNullOrWhiteSpace(taskCompleteDto.TaskId)) return aGVResponse.Error("任务号不能为空");
                if (string.IsNullOrWhiteSpace(taskCompleteDto.DevId)) return aGVResponse.Error("设备编号不能为空");
                if (taskCompleteDto.InOut != 1 && taskCompleteDto.InOut != 2) return aGVResponse.Error($"出入库类型错误,必须为1(入库)或2(出库),当前值:{taskCompleteDto.InOut}");
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCompleteDto.TaskId);
                if (task == null) return aGVResponse.Error($"未找到任务信息,任务号:{taskCompleteDto.TaskId}");
                if (taskCompleteDto.InOut == 2)
                {
                    var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
                    if (stockInfo == null) return aGVResponse.Error($"未找到托盘{task.PalletCode}的库存信息");
                    var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
                    if (locationInfo == null) return aGVResponse.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
                    if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                    {
                        return aGVResponse.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
                    }
                    locationInfo.LocationStatus = (int)LocationStatusEnum.Free;
                    task.TaskStatus = (int)TaskOutStatusEnum.OutFinish;
                    _unitOfWorkManage.BeginTran();
                    _stockInfoService.DeleteData(stockInfo);
                    await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
                    BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                    _unitOfWorkManage.CommitTran();
                }
                else
                {
                    //查找可用货位
                    var availableLocation = await _locationInfoService.GetLocationInfo(task.Roadway);
                    if (availableLocation == null) return aGVResponse.Error("无可用的入库货位");
                    task.TargetAddress = availableLocation.LocationCode;
                    // é€šçŸ¥WCS入库
                    var wcsTaskDto = _mapper.Map<WCSTaskDTO>(task);
                    var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, JsonSerializer.Serialize(wcsTaskDto));
                    if (httpResponse == null) return aGVResponse.Error("下发WCS失败");
                    task.TaskStatus = (int)TaskStatusEnum.Line_Executing;
                    task.Dispatchertime = DateTime.Now;
                    var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
                    if (locationInfo == null) return aGVResponse.Error($"未找到托盘{task.TargetAddress}的货位信息");
                    if (locationInfo.LocationStatus != (int)LocationStatusEnum.InStock)
                    {
                        return aGVResponse.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
                    }
                    Dt_StockInfo dt_Stock = new Dt_StockInfo()
                    {
                        PalletCode = task.PalletCode,
                        StockStatus = (int)StockStatusEmun.入库确认,
                        LocationCode = locationInfo.LocationCode,
                        WarehouseId = task.WarehouseId,
                        Creater = "AGV",
                        CreateDate = DateTime.Now
                    };
                    locationInfo.LocationStatus = (int)LocationStatusEnum.FreeLock;
                    _unitOfWorkManage.BeginTran();
                    BaseDal.UpdateData(task);
                    _locationInfoService.UpdateData(locationInfo);
                    _stockInfoService.AddData(dt_Stock);
                    _unitOfWorkManage.CommitTran();
                }
                return aGVResponse.OK();
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return aGVResponse.Error($"WMS取放货完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// ä»»åŠ¡å–æ¶ˆ
        /// </summary>
        /// <param name="taskCancelDto">请求参数</param>
        /// <returns></returns>
        public async Task<AGVResponse> TaskCancelAsync(TaskCancelDto taskCancelDto)
        {
            AGVResponse aGVResponse = new AGVResponse();
            try
            {
                if (taskCancelDto == null) return aGVResponse.Error("请求参数不能为空");
                if (string.IsNullOrWhiteSpace(taskCancelDto.TaskId)) return aGVResponse.Error("任务号不能为空");
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCancelDto.TaskId);
                // æ²¡æœ‰ä»»åŠ¡å¼ºåˆ¶å–æ¶ˆ
                if (task == null) return aGVResponse.OK();
                if (task.TaskStatus == (int)TaskInStatusEnum.InNew)
                {
                    task.TaskStatus = (int)TaskInStatusEnum.InCancel;
                    // å…¥åº“任务直接删除
                    _unitOfWorkManage.BeginTran();
                    BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                    _unitOfWorkManage.CommitTran();
                    return aGVResponse.OK();
                }
                else if (task.TaskStatus == (int)TaskOutStatusEnum.OutNew)
                {
                    // å‡ºåº“任务恢复库存
                    var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
                    if (stockInfo == null) return aGVResponse.Error($"未找到托盘{task.PalletCode}的库存信息");
                    var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
                    if (locationInfo == null) return aGVResponse.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
                    if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                    {
                        return aGVResponse.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
                    }
                    stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
                    locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
                    task.TaskStatus = (int)TaskOutStatusEnum.OutCancel;
                    _unitOfWorkManage.BeginTran();
                    _locationInfoService.UpdateData(locationInfo);
                    _stockInfoService.UpdateData(stockInfo);
                    BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                    _unitOfWorkManage.CommitTran();
                    return aGVResponse.OK();
                }
                return aGVResponse.Error("任务已经在执行中,不可取消");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return aGVResponse.Error($"WMS任务取消接口错误:{ex.Message}");
            }
        }
        #endregion æžå·åº“任务模块
    }
}
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/Task_HtyService.cs
@@ -11,6 +11,11 @@
    public class Task_HtyService : ServiceBase<Dt_Task_Hty, IRepository<Dt_Task_Hty>>, ITask_HtyService
    {
        /// <summary>
        /// èŽ·å–ä»»åŠ¡åŽ†å²ä»“å‚¨æŽ¥å£
        /// </summary>
        public IRepository<Dt_Task_Hty> Repository => BaseDal;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="baseDal">基础数据访问对象</param>
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,199 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using WIDESEA_Core.BaseRepository;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
using WIDESEA_WMSServer.Hubs;
namespace WIDESEA_WMSServer.BackgroundServices
{
    /// <summary>
    /// åº“存监控后台服务
    /// å®šæœŸæ£€æŸ¥åº“存和货位数据变化并通过SignalR推送到前端
    /// </summary>
    public class StockMonitorBackgroundService : BackgroundService
    {
        private readonly ILogger<StockMonitorBackgroundService> _logger;
        private readonly IHubContext<StockHub> _hubContext;
        private readonly IServiceProvider _serviceProvider;
        // è´§ä½çŠ¶æ€å¿«ç…§ï¼škey = LocationId
        private ConcurrentDictionary<int, LocationSnapshot> _lastLocationSnapshots = new();
        // ç›‘控间隔(毫秒)
        private const int MonitorIntervalMs = 3000;
        public StockMonitorBackgroundService(
            ILogger<StockMonitorBackgroundService> logger,
            IHubContext<StockHub> hubContext,
            IServiceProvider serviceProvider)
        {
            _logger = logger;
            _hubContext = hubContext;
            _serviceProvider = serviceProvider;
        }
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("库存监控后台服务已启动");
            // ç­‰å¾…应用完全启动
            await Task.Delay(5000, stoppingToken);
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await CheckChangesAsync();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "检查数据变化时发生错误");
                }
                await Task.Delay(MonitorIntervalMs, stoppingToken);
            }
            _logger.LogInformation("库存监控后台服务已停止");
        }
        /// <summary>
        /// æ£€æŸ¥è´§ä½å’Œåº“存变化
        /// </summary>
        private async Task CheckChangesAsync()
        {
            using var scope = _serviceProvider.CreateScope();
            var stockService = scope.ServiceProvider.GetRequiredService<IStockInfoService>();
            var locationRepo = scope.ServiceProvider.GetRequiredService<IRepository<Dt_LocationInfo>>();
            // 1. èŽ·å–æ‰€æœ‰è´§ä½æ•°æ®
            var allLocations = await locationRepo.QueryDataAsync(x => x.LocationStatus != 99); // æŽ’除禁用的货位
            // 2. èŽ·å–æ‰€æœ‰åº“å­˜æ•°æ®ï¼ˆåŒ…å«æ˜Žç»†ï¼‰
            var allStockData = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
                .Includes(x => x.Details)
                .ToListAsync();
            // æž„建库存字典:LocationId -> StockInfo
            var stockDict = allStockData
                .Where(s => s.LocationId > 0)
                .ToDictionary(s => s.LocationId, s => s);
            // æž„建当前货位快照字典
            var currentSnapshots = new ConcurrentDictionary<int, LocationSnapshot>();
            foreach (var location in allLocations)
            {
                // èŽ·å–è¯¥è´§ä½çš„åº“å­˜ä¿¡æ¯
                stockDict.TryGetValue(location.Id, out var stock);
                // è®¡ç®—库存数量
                float totalQuantity = 0;
                string detailsHash = string.Empty;
                if (stock?.Details != null && stock.Details.Any())
                {
                    totalQuantity = stock.Details.Sum(d => d.StockQuantity);
                    detailsHash = GenerateDetailsHash(stock.Details.ToList());
                }
                var snapshot = new LocationSnapshot
                {
                    LocationId = location.Id,
                    WarehouseId = location.WarehouseId,
                    LocationCode = location.LocationCode,
                    LocationStatus = location.LocationStatus,
                    PalletCode = stock?.PalletCode,
                    StockStatus = stock?.StockStatus ?? 0,
                    StockQuantity = totalQuantity,
                    DetailsHash = detailsHash
                };
                currentSnapshots.TryAdd(location.Id, snapshot);
                // æ£€æŸ¥æ˜¯å¦æœ‰å˜åŒ–
                if (_lastLocationSnapshots.TryGetValue(location.Id, out var lastSnapshot))
                {
                    // æ£€æµ‹å˜åŒ–:货位状态、库存状态、数量、明细变化
                    if (lastSnapshot.LocationStatus != snapshot.LocationStatus ||
                        lastSnapshot.StockStatus != snapshot.StockStatus ||
                        lastSnapshot.PalletCode != snapshot.PalletCode ||
                        Math.Abs(lastSnapshot.StockQuantity - snapshot.StockQuantity) > 0.001f ||
                        lastSnapshot.DetailsHash != snapshot.DetailsHash)
                    {
                        // æž„建更新DTO
                        var update = new StockUpdateDTO
                        {
                            LocationId = snapshot.LocationId,
                            WarehouseId = snapshot.WarehouseId,
                            PalletCode = snapshot.PalletCode,
                            StockQuantity = snapshot.StockQuantity,
                            StockStatus = snapshot.StockStatus,
                            LocationStatus = snapshot.LocationStatus,
                            Details = BuildDetailDtos(stock?.Details?.ToList())
                        };
                        await _hubContext.Clients.All.SendAsync("StockUpdated", update);
                        _logger.LogDebug("数据变化推送: LocationId={LocationId}, LocStatus={LocStatus}, StockStatus={StockStatus}, Quantity={Quantity}",
                            snapshot.LocationId, snapshot.LocationStatus, snapshot.StockStatus, snapshot.StockQuantity);
                    }
                }
            }
            // æ›´æ–°å¿«ç…§æ•°æ®
            _lastLocationSnapshots = currentSnapshots;
        }
        /// <summary>
        /// ç”Ÿæˆæ˜Žç»†æ•°æ®å“ˆå¸Œ
        /// </summary>
        private string GenerateDetailsHash(List<Dt_StockInfoDetail> details)
        {
            if (details == null || !details.Any()) return string.Empty;
            var hashString = string.Join("|", details
                .OrderBy(d => d.Id)
                .Select(d => $"{d.Id}:{d.MaterielCode}:{d.BatchNo}:{d.StockQuantity}"));
            return hashString.GetHashCode().ToString();
        }
        /// <summary>
        /// æž„建明细DTO列表
        /// </summary>
        private List<StockDetailUpdateDTO> BuildDetailDtos(List<Dt_StockInfoDetail> details)
        {
            if (details == null || !details.Any()) return new List<StockDetailUpdateDTO>();
            return details.Select(d => new StockDetailUpdateDTO
            {
                Id = d.Id,
                MaterielCode = d.MaterielCode,
                MaterielName = d.MaterielName,
                BatchNo = d.BatchNo,
                StockQuantity = d.StockQuantity,
                Unit = d.Unit,
                Status = d.Status
            }).ToList();
        }
        /// <summary>
        /// è´§ä½å¿«ç…§
        /// </summary>
        private class LocationSnapshot
        {
            public int LocationId { get; set; }
            public int WarehouseId { get; set; }
            public string LocationCode { get; set; }
            public int LocationStatus { get; set; }
            public string PalletCode { get; set; }
            public int StockStatus { get; set; }
            public float StockQuantity { get; set; }
            public string DetailsHash { get; set; }
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Basic/WarehouseController.cs
@@ -18,5 +18,15 @@
        {
        }
        /// <summary>
        /// èŽ·å–æ‰€æœ‰ä»“åº“
        /// </summary>
        /// <returns>仓库列表</returns>
        [HttpGet("GetAll")]
        public async Task<WebResponseContent> GetAll()
        {
            var result = await Service.Repository.QueryDataAsync(x => x.WarehouseStatus == 1);
            return WebResponseContent.Instance.OK(data: result);
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,271 @@
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// ä»ªè¡¨ç›˜
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// æ€»è§ˆæ•°æ®
        /// </summary>
        [HttpGet("Overview")]
        public async Task<WebResponseContent> Overview()
        {
            try
            {
                var today = DateTime.Today;
                var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
                // ä»Šæ—¥å…¥åº“æ•°
                var todayInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // ä»Šæ—¥å‡ºåº“æ•°
                var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // æœ¬æœˆå…¥åº“æ•°
                var monthInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // æœ¬æœˆå‡ºåº“æ•°
                var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // å½“前总库存
                var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
                return WebResponseContent.Instance.OK(null, new
                {
                    TodayInbound = todayInbound,
                    TodayOutbound = todayOutbound,
                    MonthInbound = monthInbound,
                    MonthOutbound = monthOutbound,
                    TotalStock = totalStock
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"总览数据获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ¯æ—¥ç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æ³¨æ„ï¼šæ•°æ®åœ¨ SQL å±‚过滤后,在应用层按日期分组。
        /// SqlSugar çš„ GroupBy ä¸æ”¯æŒå¯¹ .Date è¿™æ ·çš„计算列直接生成 SQL GROUP BY,
        /// å› æ­¤é‡‡ç”¨æ­¤æ–¹å¼ä»¥ç¡®ä¿è·¨æ•°æ®åº“兼容性。
        /// </remarks>
        [HttpGet("DailyStats")]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            try
            {
                if (days <= 0) days = 30;
                if (days > 365) days = 365;
                var startDate = DateTime.Today.AddDays(-days + 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => t.InsertTime.Date)
                    .Select(g => new
                    {
                        Date = g.Key.ToString("yyyy-MM-dd"),
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Date)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// æ¯å‘¨ç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æ³¨æ„ï¼šæ•°æ®åœ¨ SQL å±‚过滤后,在应用层按 ISO 8601 å‘¨é”®åˆ†ç»„。
        /// å‘¨é”®ä¸º "YYYY-Www" æ ¼å¼ï¼Œæ— æ³•直接在 SQL å±‚用 GROUP BY å®žçŽ°ã€‚
        /// </remarks>
        [HttpGet("WeeklyStats")]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            try
            {
                if (weeks <= 0) weeks = 12;
                var startDate = DateTime.Today.AddDays(-weeks * 7);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => GetWeekKey(t.InsertTime))
                    .Select(g => new
                    {
                        Week = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Week)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每周统计获取失败: {ex.Message}");
            }
        }
        private string GetWeekKey(DateTime date)
        {
            // èŽ·å–å‘¨ä¸€å¼€å§‹çš„å‘¨ (ISO 8601)
            var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
            var monday = date.AddDays(-diff);
            var weekNum = System.Globalization.CultureInfo.InvariantCulture
                .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            return $"{monday.Year}-W{weekNum:D2}";
        }
        /// <summary>
        /// æ¯æœˆç»Ÿè®¡
        /// </summary>
        /// <remarks>
        /// æ³¨æ„ï¼šæ•°æ®åœ¨ SQL å±‚过滤后,在应用层按年月分组。
        /// SqlSugar çš„ GroupBy ä¸æ”¯æŒåŒ¿åå¯¹è±¡ (Year, Month) ç›´æŽ¥æ˜ å°„到 SQL GROUP BY,
        /// å› æ­¤é‡‡ç”¨æ­¤æ–¹å¼ä»¥ç¡®ä¿è·¨æ•°æ®åº“兼容性。
        /// </remarks>
        [HttpGet("MonthlyStats")]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            try
            {
                if (months <= 0) months = 12;
                var startDate = DateTime.Today.AddMonths(-months + 1);
                startDate = new DateTime(startDate.Year, startDate.Month, 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
                    .Select(g => new
                    {
                        Month = $"{g.Key.Year}-{g.Key.Month:D2}",
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Month)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// åº“存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution")]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            try
            {
                var today = DateTime.Today;
                // ä½¿ç”¨ SQL ç›´æŽ¥åˆ†ç»„统计,避免加载所有数据到内存
                var result = new[]
                {
                    new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 7).CountAsync() },
                    new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 7 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 30).CountAsync() },
                    new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 30 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 90).CountAsync() },
                    new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 90).CountAsync() }
                };
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"库存库龄分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// å„仓库库存分布
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ SQL GROUP BY åœ¨æ•°æ®åº“层面聚合,避免加载全部库存记录到内存。
        /// </remarks>
        [HttpGet("StockByWarehouse")]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            try
            {
                // æŸ¥è¯¢ä»“库名称
                var warehouses = await _db.Queryable<Dt_Warehouse>()
                    .Select(w => new { w.WarehouseId, w.WarehouseName })
                    .ToListAsync();
                var warehouseDict = warehouses.ToDictionary(w => w.WarehouseId, w => w.WarehouseName);
                // ä½¿ç”¨ SQL GROUP BY åœ¨æ•°æ®åº“层面聚合,仅返回聚合结果
                var stockGroups = await _db.Queryable<Dt_StockInfo>()
                    .GroupBy(s => s.WarehouseId)
                    .Select(s => new { s.WarehouseId, Count = SqlFunc.AggregateCount(s.Id) })
                    .ToListAsync();
                var result = stockGroups
                    .Select(g => new
                    {
                        Warehouse = warehouseDict.TryGetValue(g.WarehouseId, out var name) ? name : $"仓库{g.WarehouseId}",
                        Count = g.Count
                    })
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs
@@ -20,6 +20,16 @@
        {
        }
        /// <summary>
        /// èŽ·å–ä»“åº“3D布局
        /// </summary>
        /// <param name="warehouseId">仓库ID</param>
        /// <returns>3D布局数据</returns>
        [HttpGet("Get3DLayout")]
        public async Task<WebResponseContent> Get3DLayout(int warehouseId)
        {
            var result = await Service.Get3DLayoutAsync(warehouseId);
            return WebResponseContent.Instance.OK(data: result);
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.DirectoryServices.Protocols;
using WIDESEA_Common.CommonEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseController;
@@ -198,5 +199,71 @@
        {
            return await Service.GetPalletCodeCellAsync(input);
        }
        #region æžå·åº“任务模块
        /// <summary>
        /// å‡ºå…¥åº“申请
        /// </summary>
        /// <param name="applyInOutDto">请求参数</param>
        /// <returns></returns>
        [HttpPost("ApplyInOut"), AllowAnonymous]
        public async Task<AGVResponse> ApplyInOutAsync([FromBody] ApplyInOutDto applyInOutDto)
        {
            return await Service.ApplyInOutAsync(applyInOutDto);
        }
        /// <summary>
        /// æ‰‹åŠ¨å‡ºåº“å®Œæˆåé¦ˆç»™AGV
        /// </summary>
        /// <param name="outTaskCompleteDto">请求参数</param>
        /// <returns></returns>
        [HttpPost, Route("OutTaskComplete"), AllowAnonymous]
        public async Task<WebResponseContent> OutTaskComplete([FromBody] OutTaskCompleteDto outTaskCompleteDto)
        {
            return await Service.OutTaskComplete(outTaskCompleteDto);
        }
        ///// <summary>
        ///// ä»»åŠ¡å®Œæˆ
        ///// </summary>
        ///// <param name="wCSTask">请求参数</param>
        ///// <returns></returns>
        //[HttpPost, Route("TaskCompleted"), AllowAnonymous]
        //public async Task<WebResponseContent?> TaskCompleted([FromBody] WCSTaskDTO wCSTask)
        //{
        //    return await Service.TaskCompleted(wCSTask);
        //}
        /// <summary>
        /// è¾“送线申请进入
        /// </summary>
        /// <param name="applyEnterDto">请求参数</param>
        /// <returns></returns>
        [HttpPost("ApplyEnter"), AllowAnonymous]
        public async Task<AGVResponse?> ApplyEnterAsync([FromBody] ApplyEnterDto applyEnterDto)
        {
            return await Service.ApplyEnterAsync(applyEnterDto);
        }
        /// <summary>
        /// å–放货完成
        /// </summary>
        /// <param name="taskCompleteDto">请求参数</param>
        /// <returns></returns>
        [HttpPost("TaskComplete"), AllowAnonymous]
        public async Task<AGVResponse?> TaskCompleteAsync([FromBody] TaskCompleteDto taskCompleteDto)
        {
            return await Service.TaskCompleteAsync(taskCompleteDto);
        }
        /// <summary>
        /// ä»»åŠ¡å–æ¶ˆ
        /// </summary>
        /// <param name="taskCancelDto">请求参数</param>
        /// <returns></returns>
        [HttpPost("TaskCancel"), AllowAnonymous]
        public async Task<AGVResponse?> TaskCancelAsync([FromBody] TaskCancelDto taskCancelDto)
        {
            return await Service.TaskCancelAsync(taskCancelDto);
        }
        #endregion æžå·åº“任务模块
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.SignalR;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WIDESEA_WMSServer.Hubs
{
    public class StockHub : Hub
    {
        /// <summary>
        /// å‘送库存更新
        /// </summary>
        public async Task SendStockUpdate(StockUpdateDTO update)
        {
            await Clients.All.SendAsync("StockUpdated", update);
        }
    }
    /// <summary>
    /// åº“存更新DTO(SignalR推送用)
    /// </summary>
    public class StockUpdateDTO
    {
        public int LocationId { get; set; }
        public int WarehouseId { get; set; }
        public string PalletCode { get; set; }
        public float StockQuantity { get; set; }
        public int StockStatus { get; set; }
        public int LocationStatus { get; set; }
        public List<StockDetailUpdateDTO> Details { get; set; } = new();
    }
    /// <summary>
    /// åº“存明细更新DTO
    /// </summary>
    public class StockDetailUpdateDTO
    {
        public int Id { get; set; }
        public string MaterielCode { get; set; }
        public string MaterielName { get; set; }
        public string BatchNo { get; set; }
        public float StockQuantity { get; set; }
        public string Unit { get; set; }
        public int Status { get; set; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -81,6 +81,7 @@
builder.Services.AddDbSetup(); // Db æ•°æ®åº“配置
builder.Services.AddInitializationHostServiceSetup(); // åº”用程序初始化服务注册
builder.Services.AddHostedService<AutoOutboundTaskBackgroundService>();  // å¯åŠ¨è‡ªåŠ¨å‡ºåº“ä»»åŠ¡åŽå°æœåŠ¡
builder.Services.AddHostedService<StockMonitorBackgroundService>();  // å¯åŠ¨åº“å­˜ç›‘æŽ§åŽå°æœåŠ¡
// builder.Services.AddHostedService<PermissionDataHostService>(); // æƒé™æ•°æ®æœåŠ¡
builder.Services.AddAutoMapperSetup();
@@ -98,6 +99,8 @@
    options.Filters.Add(typeof(ApiAuthorizeFilter));
    options.Filters.Add(typeof(ActionExecuteFilter));
});
builder.Services.AddSignalR();
builder.Services.AddScoped<HttpClientHelper>();
@@ -174,4 +177,6 @@
app.MapControllers();
app.MapHub<WIDESEA_WMSServer.Hubs.StockHub>("/stockHub");
app.Run();
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
@@ -35,6 +35,7 @@
  //连接字符串
  //"ConnectionString": "HTI6FB1H05Krd07mNm9yBCNhofW6edA5zLs9TY~MNthRYW3kn0qKbMIsGp~3yyPDF1YZUCPBQx8U0Jfk4PH~ajNFXVIwlH85M3F~v_qKYQ3CeAz3q1mLVDn8O5uWt1~3Ut2V3KRkEwYHvW2oMDN~QIDXPxDgXN0R2oTIhc9dNu7QNaLEknblqmHhjaNSSpERdDVZIgHnMKejU_SL49tralBkZmDNi0hmkbL~837j1NWe37u9fJKmv91QPb~16JsuI9uu0EvNZ06g6PuZfOSAeFH9GMMIZiketdcJG3tHelo=",
  "ConnectionString": "Data Source=.;Initial Catalog=WIDESEAWMS_ShanMei;User ID=sa;Password=P@ssw0rd;Integrated Security=False;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
  //"ConnectionString": "Data Source=.;Initial Catalog=WIDESEAWMS_ShanMei;User ID=sa;Password=123456;Integrated Security=False;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
  //"ConnectionString": "Data Source=10.30.4.92;Initial Catalog=WMS_TC;User ID=sa;Password=duo123456;Integrated Security=False;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
  //旧WMS数据库连接
  //"TeConnectionString": "Data Source=10.30.4.92;Initial Catalog=TeChuang;User ID=sa;Password=duo123456;Integrated Security=False;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
@@ -72,5 +73,7 @@
  "MES": {
    "BaseUrl": "http://localhost:5000",
    "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMwMTcyNzM5Mzk5NzYxOTIwIiwibmFtZSI6IlBBQ0voo4XphY3lt6XkvY0wMSIsIkZhY3RvcnlJZCI6IjEyMzQ1NiIsIlNpdGVJZCI6IjEyMzQ1NiIsIkNvZGUiOiJYWExQQUNLMDRBRTAzMiIsIm5iZiI6MTcwNDE4NzY5MCwiZXhwIjoyMTQ1NjkxNjkwLCJpc3MiOiJodHRwczovL3d3dy5oeW1zb24uY29tIiwiYXVkIjoiaHR0cHM6Ly93d3cuaHltc29uLmNvbSJ9.An1BE7UgfcSP--LtTOmmmWVE2RQFPDahLkDg1xy5KqY"
  }
  },
  "AGV_OutTaskComplete": "http://localhost:9999/OutTaskComplete", // ä¸ŠæŠ¥AGV出库输送线完成
  "WCS_ReceiveTask": "http://localhost:9292/api/Task/ReceiveTask" // WMS输送线任务下发
}
Code/WMS/docs/superpowers/plans/2026-03-30-MESÍÐÅ̽øÕ¾³öÕ¾¼¯³ÉʵÏּƻ®.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,297 @@
# MES æ‰˜ç›˜è¿›ç«™å‡ºç«™é›†æˆå®žçŽ°è®¡åˆ’
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** åœ¨ TaskService çš„入库完成/出库完成方法中集成 MES è¿›ç«™/出站调用,新增空托盘入库/出库完成方法。
**Architecture:** åœ¨ `ExecuteWithinTransactionAsync` äº‹åŠ¡å†…æ·»åŠ  MES è°ƒç”¨ï¼ŒMES å¤±è´¥åˆ™äº‹åŠ¡å›žæ»šã€‚
**Tech Stack:** ASP.NET Core 6.0, IMesService, ExecuteWithinTransactionAsync
---
## ä»»åŠ¡æ€»è§ˆ
| ä»»åŠ¡ | æ–¹æ³• | æ“ä½œ |
|------|------|------|
| Task 1 | `InboundFinishTaskAsync` | æ·»åŠ  `InboundInContainer` è°ƒç”¨ |
| Task 2 | `OutboundFinishTaskAsync` | æ·»åŠ  `OutboundInContainer` è°ƒç”¨ |
| Task 3 | `InboundFinishTaskTrayAsync`(新增) | ç©ºæ‰˜ç›˜å…¥åº“完成,无需 MES |
| Task 4 | `OutboundFinishTaskTrayAsync`(新增) | ç©ºæ‰˜ç›˜å‡ºåº“完成,无需 MES |
---
## ä»»åŠ¡å‰ç½®æ¡ä»¶
`TaskService` éœ€æ³¨å…¥ `IMesService`。检查现有构造函数是否已有:
```csharp
private readonly IMesService _mesService;
```
如果不存在,需添加。
---
## Task 1: ä¿®æ”¹ InboundFinishTaskAsync æ·»åŠ  MES è¿›ç«™è°ƒç”¨
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`(`InboundFinishTaskAsync` æ–¹æ³•,约第 215 è¡Œï¼‰
- [ ] **Step 1: æŸ¥çœ‹å½“前代码确认上下文**
读取 `TaskService.cs` ç¬¬ 199-240 è¡Œï¼Œç¡®è®¤ `CompleteTaskAsync` è°ƒç”¨çš„位置。
- [ ] **Step 2: åœ¨ CompleteTaskAsync ä¹‹å‰æ·»åŠ  MES InboundInContainer è°ƒç”¨**
在 `return await CompleteTaskAsync(task);` ä¹‹å‰æ·»åŠ ï¼š
```csharp
// è°ƒç”¨MES托盘进站
var inboundRequest = new InboundInContainerRequest
{
    EquipmentCode = "STK-GROUP-001",
    ResourceCode = "STK-GROUP-001",
    LocalTime = DateTime.Now,
    ContainerCode = taskDto.PalletCode
};
var inboundResult = _mesService.InboundInContainer(inboundRequest);
if (inboundResult == null || inboundResult.Data == null || !inboundResult.Data.IsSuccess)
{
    return content.Error($"任务完成失败:MES进站失败: {inboundResult?.Data?.Msg ?? inboundResult?.ErrorMessage ?? "未知错误"}");
}
```
- [ ] **Step 3: æ·»åŠ  using å¼•用(如果需要)**
确认文件顶部已有 `using WIDESEA_IBasicService;` å’Œ `using WIDESEA_DTO.MES;`。如果没有,添加。
- [ ] **Step 4: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
确认无编译错误。
- [ ] **Step 5: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): InboundFinishTaskAsync添加MES进站调用"
```
---
## Task 2: ä¿®æ”¹ OutboundFinishTaskAsync æ·»åŠ  MES å‡ºç«™è°ƒç”¨
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`(`OutboundFinishTaskAsync` æ–¹æ³•,约第 258 è¡Œï¼‰
- [ ] **Step 1: æŸ¥çœ‹å½“前代码确认上下文**
读取 `TaskService.cs` ç¬¬ 258-280 è¡Œï¼Œç¡®è®¤ `CompleteTaskAsync` è°ƒç”¨çš„位置。
- [ ] **Step 2: åœ¨ CompleteTaskAsync ä¹‹å‰æ·»åŠ  MES OutboundInContainer è°ƒç”¨**
在 `return await CompleteTaskAsync(task);` ä¹‹å‰æ·»åŠ ï¼š
```csharp
// è°ƒç”¨MES托盘出站
var outboundRequest = new OutboundInContainerRequest
{
    EquipmentCode = "STK-GROUP-001",
    ResourceCode = "STK-GROUP-001",
    LocalTime = DateTime.Now,
    ContainerCode = taskDto.PalletCode,
    ParamList = new List<ParamItem>()
};
var outboundResult = _mesService.OutboundInContainer(outboundRequest);
if (outboundResult == null || outboundResult.Data == null || !outboundResult.Data.IsSuccess)
{
    return content.Error($"任务完成失败:MES出站失败: {outboundResult?.Data?.Msg ?? outboundResult?.ErrorMessage ?? "未知错误"}");
}
```
- [ ] **Step 3: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
确认无编译错误。
- [ ] **Step 4: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): OutboundFinishTaskAsync添加MES出站调用"
```
---
## Task 3: æ–°å¢ž InboundFinishTaskTrayAsync ç©ºæ‰˜ç›˜å…¥åº“完成方法
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`(在 `InboundFinishTaskTrayAsync` æ–¹æ³•之后添加新方法)
- [ ] **Step 1: æŸ¥çœ‹çŽ°æœ‰ InboundFinishTaskTrayAsync æ–¹æ³•位置**
读取 `TaskService.cs` ç¬¬ 330-350 è¡Œï¼Œç¡®è®¤ `CreateTaskInboundTrayAsync` ä¹‹åŽçš„位置。
- [ ] **Step 2: æ·»åŠ æ–°æ–¹æ³• InboundFinishTaskTrayAsync**
在 `CreateTaskInboundTrayAsync` æ–¹æ³•之后添加:
```csharp
/// <summary>
/// ç©ºæ‰˜ç›˜å…¥åº“完成
/// </summary>
public async Task<WebResponseContent> InboundFinishTaskTrayAsync(CreateTaskDto taskDto)
{
    try
    {
        var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
        if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
        var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.TargetAddress);
        if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
        var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
        if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
        return await ExecuteWithinTransactionAsync(async () =>
        {
            stockInfo.LocationCode = location.LocationCode;
            stockInfo.LocationId = location.Id;
            stockInfo.StockStatus = StockStatusEmun.空托盘库存.GetHashCode();
            location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
            var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
            var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
            if (!updateLocationResult || !updateStockResult)
                return WebResponseContent.Instance.Error("任务完成失败");
            var deleteResult = await BaseDal.DeleteDataAsync(task);
            if (!deleteResult) return WebResponseContent.Instance.Error("任务完成失败");
            return WebResponseContent.Instance.OK("任务完成");
        });
    }
    catch (Exception ex)
    {
        return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
    }
}
```
- [ ] **Step 3: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
确认无编译错误。
- [ ] **Step 4: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): æ–°å¢žInboundFinishTaskTrayAsync空托盘入库完成方法"
```
---
## Task 4: æ–°å¢ž OutboundFinishTaskTrayAsync ç©ºæ‰˜ç›˜å‡ºåº“完成方法
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`(在 `OutboundFinishTaskTrayAsync` æ–¹æ³•之后添加新方法)
- [ ] **Step 1: æŸ¥çœ‹çŽ°æœ‰ GetOutBoundTrayTaskAsync æ–¹æ³•位置**
读取 `TaskService.cs` ç¬¬ 357-393 è¡Œï¼Œç¡®è®¤ `GetOutBoundTrayTaskAsync` ä¹‹åŽçš„位置。
- [ ] **Step 2: æ·»åŠ æ–°æ–¹æ³• OutboundFinishTaskTrayAsync**
在 `GetOutBoundTrayTaskAsync` æ–¹æ³•之后添加:
```csharp
/// <summary>
/// ç©ºæ‰˜ç›˜å‡ºåº“完成
/// </summary>
public async Task<WebResponseContent> OutboundFinishTaskTrayAsync(CreateTaskDto taskDto)
{
    try
    {
        var task = await BaseDal.QueryFirstAsync(s => s.PalletCode == taskDto.PalletCode);
        if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务");
        var location = await _locationInfoService.GetLocationInfo(task.Roadway, task.SourceAddress);
        if (location == null) return WebResponseContent.Instance.Error("未找到对应的货位");
        var stockInfo = await _stockInfoService.GetStockInfoAsync(taskDto.PalletCode);
        if (stockInfo == null) return WebResponseContent.Instance.Error("未找到对应库存信息");
        return await ExecuteWithinTransactionAsync(async () =>
        {
            stockInfo.LocationId = 0;
            stockInfo.LocationCode = null;
            stockInfo.StockStatus = StockStatusEmun.出库完成.GetHashCode();
            location.LocationStatus = LocationStatusEnum.Free.GetHashCode();
            var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
            var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
            if (!updateLocationResult || !updateStockResult)
                return WebResponseContent.Instance.Error("任务完成失败");
            var deleteResult = await BaseDal.DeleteDataAsync(task);
            if (!deleteResult) return WebResponseContent.Instance.Error("任务完成失败");
            return WebResponseContent.Instance.OK("任务完成");
        });
    }
    catch (Exception ex)
    {
        return WebResponseContent.Instance.Error($"完成任务失败: {ex.Message}");
    }
}
```
- [ ] **Step 3: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
确认无编译错误。
- [ ] **Step 4: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): æ–°å¢žOutboundFinishTaskTrayAsync空托盘出库完成方法"
```
---
## Task 5: æ•´ä½“构建验证
- [ ] **Step 1: æž„建整个解决方案**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_WMSServer.sln
```
确认无编译错误、无警告。
---
## éªŒè¯æ£€æŸ¥æ¸…单
- [ ] `InboundFinishTaskAsync` ä¸­ `InboundInContainer` åœ¨ `CompleteTaskAsync` ä¹‹å‰
- [ ] `OutboundFinishTaskAsync` ä¸­ `OutboundInContainer` åœ¨ `CompleteTaskAsync` ä¹‹å‰
- [ ] æ‰€æœ‰ MES è°ƒç”¨æ£€æŸ¥ `mesResult.Data?.IsSuccess`
- [ ] é”™è¯¯ä¿¡æ¯æ ¼å¼ï¼š`"任务完成失败:MES{操作}失败: {错误信息}"`
- [ ] `InboundFinishTaskTrayAsync` å’Œ `OutboundFinishTaskTrayAsync` æ–°å¢žæ–¹æ³•签名正确
- [ ] è§£å†³æ–¹æ¡ˆæž„建无错误
Code/WMS/docs/superpowers/plans/2026-03-30-MESµçо°ó¶¨½â°ó¼¯³ÉʵÏּƻ®.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,213 @@
# MES ç”µèŠ¯ç»‘å®šè§£ç»‘é›†æˆå®žçŽ°è®¡åˆ’
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** åœ¨ StockService çš„组盘、换盘、拆盘操作中正确调用 MES ç”µèŠ¯ç»‘å®š/解绑接口,并将 MES è°ƒç”¨çº³å…¥ WMS äº‹åŠ¡å†…ã€‚
**Architecture:** ä¿®æ”¹ `StockService` ä¸‰ä¸ªæ–¹æ³•,将 `_mesService.BindContainer()` / `_mesService.UnBindContainer()` æ­£ç¡®åœ°ç”¨ `await` è°ƒç”¨å¹¶æ£€æŸ¥è¿”回结果,MES å¤±è´¥åˆ™äº‹åŠ¡å›žæ»šã€‚
**Tech Stack:** ASP.NET Core 6.0, SqlSugar, IMesService
---
## ä»»åŠ¡æ€»è§ˆ
| ä»»åŠ¡ | æ–¹æ³• | æ“ä½œ |
|------|------|------|
| Task 1 | `GroupPalletAsync` | ä¿®å¤ `_mesService.BindContainer()` ç¼ºå°‘ await å’Œç»“果检查 |
| Task 2 | `ChangePalletAsync` | æ·»åŠ è§£ç»‘æºæ‰˜ç›˜ + ç»‘定目标托盘 |
| Task 3 | `SplitPalletAsync` | æ·»åŠ è§£ç»‘ç”µèŠ¯ |
---
## Task 1: ä¿®å¤ GroupPalletAsync ä¸­çš„ MES è°ƒç”¨
**Files:**
- Modify: `WIDESEA_StockService/StockSerivce.cs:132-176`
- [ ] **Step 1: æŸ¥çœ‹å½“前代码确认上下文**
读取 `StockSerivce.cs` ç¬¬ 132-176 è¡Œï¼Œç¡®è®¤ `bindRequest` å¯¹è±¡çš„æž„建和 `_mesService.BindContainer()` è°ƒç”¨çš„位置。
- [ ] **Step 2: ä¿®æ”¹ BindContainer è°ƒç”¨ä¸º await å¹¶æ£€æŸ¥ç»“æžœ**
将第 166 è¡Œï¼š
```csharp
_mesService.BindContainer()
```
修改为:
```csharp
var mesResult = await _mesService.BindContainer(bindRequest);
if (mesResult == null || !mesResult.Success)
{
    return content.Error($"组盘成功,但MES绑定失败: {mesResult?.Message ?? "未知错误"}");
}
```
- [ ] **Step 3: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_StockService/WIDESEA_StockService.csproj
```
确认无编译错误。
- [ ] **Step 4: æäº¤**
```bash
git add WIDESEA_StockService/StockSerivce.cs
git commit -m "fix(StockService): GroupPalletAsync正确await MES BindContainer调用并检查结果"
```
---
## Task 2: ä¿®æ”¹ ChangePalletAsync æ·»åŠ  MES è§£ç»‘和绑定调用
**Files:**
- Modify: `WIDESEA_StockService/StockSerivce.cs:181-240`
- [ ] **Step 1: æŸ¥çœ‹å½“前代码确认上下文**
读取 `StockSerivce.cs` ç¬¬ 181-240 è¡Œï¼Œç¡®è®¤ï¼š
- `detailEntities` å˜é‡å®šä¹‰ä½ç½®ï¼ˆåŒ…含要换盘的电芯明细)
- `targetStock.Id` èµ‹å€¼ä½ç½®
- `return content.OK("换盘成功")` ä¹‹å‰çš„逻辑
- [ ] **Step 2: åœ¨æ›´æ–°åº“存明细前添加 UnBindContainer è°ƒç”¨**
在第 231 è¡Œ `var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);` ä¹‹å‰æ·»åŠ ï¼š
```csharp
// è°ƒç”¨MES解绑源托盘电芯
var unbindRequest = new UnBindContainerRequest
{
    EquipmentCode = "STK-GROUP-001",
    ResourceCode = "STK-GROUP-001",
    LocalTime = DateTime.Now,
    ContainCode = stock.SourcePalletNo,
    SfcList = detailEntities.Select(d => d.SerialNumber).ToList()
};
var unbindResult = await _mesService.UnBindContainer(unbindRequest);
if (unbindResult == null || !unbindResult.Success)
{
    return content.Error($"换盘成功,但MES解绑失败: {unbindResult?.Message ?? "未知错误"}");
}
```
- [ ] **Step 3: åœ¨æ›´æ–°åº“存明细后添加 BindContainer è°ƒç”¨**
在第 231 è¡Œä¹‹åŽã€`return content.OK("换盘成功");` ä¹‹å‰æ·»åŠ ï¼š
```csharp
// è°ƒç”¨MES绑定目标托盘电芯
var bindRequest = new BindContainerRequest
{
    ContainerCode = stock.TargetPalletNo,
    EquipmentCode = "STK-GROUP-001",
    ResourceCode = "STK-GROUP-001",
    LocalTime = DateTime.Now,
    OperationType = 0,
    ContainerSfcList = detailEntities.Select(d => new ContainerSfcItem
    {
        Sfc = d.SerialNumber,
        Location = d.InboundOrderRowNo.ToString()
    }).ToList()
};
var bindResult = await _mesService.BindContainer(bindRequest);
if (bindResult == null || !bindResult.Success)
{
    return content.Error($"换盘成功,但MES绑定失败: {bindResult?.Message ?? "未知错误"}");
}
```
- [ ] **Step 4: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_StockService/WIDESEA_StockService.csproj
```
确认无编译错误。
- [ ] **Step 5: æäº¤**
```bash
git add WIDESEA_StockService/StockSerivce.cs
git commit -m "feat(StockService): ChangePalletAsync添加MES解绑和绑定调用"
```
---
## Task 3: ä¿®æ”¹ SplitPalletAsync æ·»åŠ  MES è§£ç»‘调用
**Files:**
- Modify: `WIDESEA_StockService/StockSerivce.cs:245-286`
- [ ] **Step 1: æŸ¥çœ‹å½“前代码确认上下文**
读取 `StockSerivce.cs` ç¬¬ 245-286 è¡Œï¼Œç¡®è®¤ï¼š
- `detailEntities` å˜é‡å®šä¹‰å’ŒåŒ…含的电芯列表
- `return content.OK("拆盘成功");` ä¹‹å‰çš„逻辑
- [ ] **Step 2: åœ¨åˆ é™¤åº“存明细前添加 UnBindContainer è°ƒç”¨**
在第 277 è¡Œ `var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);` ä¹‹å‰æ·»åŠ ï¼š
```csharp
// è°ƒç”¨MES解绑电芯
var unbindRequest = new UnBindContainerRequest
{
    EquipmentCode = "STK-GROUP-001",
    ResourceCode = "STK-GROUP-001",
    LocalTime = DateTime.Now,
    ContainCode = stock.SourcePalletNo,
    SfcList = detailEntities.Select(d => d.SerialNumber).ToList()
};
var unbindResult = await _mesService.UnBindContainer(unbindRequest);
if (unbindResult == null || !unbindResult.Success)
{
    return content.Error($"拆盘成功,但MES解绑失败: {unbindResult?.Message ?? "未知错误"}");
}
```
- [ ] **Step 3: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_StockService/WIDESEA_StockService.csproj
```
确认无编译错误。
- [ ] **Step 4: æäº¤**
```bash
git add WIDESEA_StockService/StockSerivce.cs
git commit -m "feat(StockService): SplitPalletAsync添加MES解绑调用"
```
---
## Task 4: æ•´ä½“构建验证
- [ ] **Step 1: æž„建整个解决方案**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_WMSServer.sln
```
确认无编译错误、无警告。
- [ ] **Step 2: æäº¤æ‰€æœ‰æ›´æ”¹**
如果之前没有合并提交,在此执行最终提交。
---
## éªŒè¯æ£€æŸ¥æ¸…单
- [ ] `GroupPalletAsync` ä¸­ `await _mesService.BindContainer()` æ­£ç¡® await
- [ ] `ChangePalletAsync` ä¸­å…ˆ UnBind å† Bind,顺序正确
- [ ] `SplitPalletAsync` ä¸­ UnBind åœ¨ Delete ä¹‹å‰
- [ ] æ‰€æœ‰ MES è°ƒç”¨æ£€æŸ¥ `Success` å±žæ€§
- [ ] é”™è¯¯ä¿¡æ¯æ ¼å¼ç»Ÿä¸€ï¼š`"{操作}成功,但MES{操作}失败: {错误信息}"`
- [ ] è§£å†³æ–¹æ¡ˆæž„建无错误
Code/WMS/docs/superpowers/plans/2026-03-30-dashboard-chart-plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,837 @@
# é¦–页仪表盘图表功能实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** åœ¨ WMS å‰ç«¯é¦–页添加仪表盘图表,展示出入库统计和库存数据
**Architecture:**
- åŽç«¯ï¼šæ–°å»º DashboardController,提供6个统计接口,使用 SqlSugar ç›´æŽ¥æŸ¥è¯¢ Dt_Task_Hty(已完成任务历史表)和 Dt_StockInfo è¡¨
- å‰ç«¯ï¼šé‡å†™ Home.vue,使用 ECharts 5.0.2 å®žçŽ°ä»ªè¡¨ç›˜å¸ƒå±€ï¼Œå¤ç”¨ bigdata.vue ä¸­çš„ ECharts ä½¿ç”¨æ¨¡å¼
- æ•°æ®æ¥æºï¼šDt_Task_Hty.InsertTime(任务完成时间),TaskType åŒºåˆ†å…¥åº“(500-599)/出库(100-199)
**Tech Stack:** ASP.NET Core 6.0, Vue 3, ECharts 5.0.2, SqlSugar
---
## æ–‡ä»¶ç»“æž„
```
后端新增:
- WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs  (仪表盘控制器)
前端修改:
- WIDESEA_WMSClient/src/views/Home.vue  (重写为仪表盘页面)
```
---
## å®žçŽ°ä»»åŠ¡
### Task 1: åˆ›å»ºåŽç«¯ DashboardController
**文件:**
- Create: `WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs`
**说明:** åˆ›å»º DashboardController,包含6个 API æŽ¥å£
- [ ] **Step 1: åˆ›å»ºæŽ§åˆ¶å™¨æ–‡ä»¶**
```csharp
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// ä»ªè¡¨ç›˜
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// æ€»è§ˆæ•°æ®
        /// </summary>
        [HttpGet("Overview")]
        public async Task<WebResponseContent> Overview()
        {
            // å®žçŽ°è§ Step 2
        }
        /// <summary>
        /// æ¯æ—¥ç»Ÿè®¡
        /// </summary>
        [HttpGet("DailyStats")]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            // å®žçŽ°è§ Step 3
        }
        /// <summary>
        /// æ¯å‘¨ç»Ÿè®¡
        /// </summary>
        [HttpGet("WeeklyStats")]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            // å®žçŽ°è§ Step 4
        }
        /// <summary>
        /// æ¯æœˆç»Ÿè®¡
        /// </summary>
        [HttpGet("MonthlyStats")]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            // å®žçŽ°è§ Step 5
        }
        /// <summary>
        /// åº“存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution")]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            // å®žçŽ°è§ Step 6
        }
        /// <summary>
        /// å„仓库库存分布
        /// </summary>
        [HttpGet("StockByWarehouse")]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            // å®žçŽ°è§ Step 7
        }
    }
}
```
- [ ] **Step 2: å®žçް Overview æŽ¥å£**
在 Overview æ–¹æ³•中实现:
```csharp
public async Task<WebResponseContent> Overview()
{
    var today = DateTime.Today;
    var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
    // ä»Šæ—¥å…¥åº“æ•°
    var todayInbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
        .CountAsync();
    // ä»Šæ—¥å‡ºåº“æ•°
    var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
        .CountAsync();
    // æœ¬æœˆå…¥åº“æ•°
    var monthInbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
        .CountAsync();
    // æœ¬æœˆå‡ºåº“æ•°
    var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
        .CountAsync();
    // å½“前总库存
    var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
    return WebResponseContent.Instance.OK(null, new
    {
        TodayInbound = todayInbound,
        TodayOutbound = todayOutbound,
        MonthInbound = monthInbound,
        MonthOutbound = monthOutbound,
        TotalStock = totalStock
    });
}
```
- [ ] **Step 3: å®žçް DailyStats æŽ¥å£**
```csharp
public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
{
    if (days <= 0) days = 30;
    if (days > 365) days = 365;
    var startDate = DateTime.Today.AddDays(-days + 1);
    var query = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= startDate)
        .Select(t => new { t.InsertTime, t.TaskType })
        .ToListAsync();
    var result = query
        .GroupBy(t => t.InsertTime.Date)
        .Select(g => new
        {
            Date = g.Key.ToString("yyyy-MM-dd"),
            Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
            Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
        })
        .OrderBy(x => x.Date)
        .ToList();
    return WebResponseContent.Instance.OK(null, result);
}
```
- [ ] **Step 4: å®žçް WeeklyStats æŽ¥å£**
```csharp
public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
{
    if (weeks <= 0) weeks = 12;
    var startDate = DateTime.Today.AddDays(-weeks * 7);
    var query = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= startDate)
        .Select(t => new { t.InsertTime, t.TaskType })
        .ToListAsync();
    var result = query
        .GroupBy(t => GetWeekKey(t.InsertTime))
        .Select(g => new
        {
            Week = g.Key,
            Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
            Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
        })
        .OrderBy(x => x.Week)
        .ToList();
    return WebResponseContent.Instance.OK(null, result);
}
private string GetWeekKey(DateTime date)
{
    // èŽ·å–å‘¨ä¸€å¼€å§‹çš„å‘¨ (ISO 8601)
    var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
    var monday = date.AddDays(-diff);
    var weekNum = System.Globalization.CultureInfo.InvariantCulture
        .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
    return $"{monday.Year}-W{weekNum:D2}";
}
```
- [ ] **Step 5: å®žçް MonthlyStats æŽ¥å£**
```csharp
public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
{
    if (months <= 0) months = 12;
    var startDate = DateTime.Today.AddMonths(-months + 1);
    startDate = new DateTime(startDate.Year, startDate.Month, 1);
    var query = await _db.Queryable<Dt_Task_Hty>()
        .Where(t => t.InsertTime >= startDate)
        .Select(t => new { t.InsertTime, t.TaskType })
        .ToListAsync();
    var result = query
        .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
        .Select(g => new
        {
            Month = $"{g.Key.Year}-{g.Key.Month:D2}",
            Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
            Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
        })
        .OrderBy(x => x.Month)
        .ToList();
    return WebResponseContent.Instance.OK(null, result);
}
```
- [ ] **Step 6: å®žçް StockAgeDistribution æŽ¥å£**
```csharp
public async Task<WebResponseContent> StockAgeDistribution()
{
    var now = DateTime.Now;
    // ä½¿ç”¨ SQL ç›´æŽ¥åˆ†ç»„统计,避免加载所有数据到内存
    var result = new[]
    {
        new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) <= 7).CountAsync() },
        new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 7 && EF.Functions.DateDiffDay(s.CreateDate, now) <= 30).CountAsync() },
        new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 30 && EF.Functions.DateDiffDay(s.CreateDate, now) <= 90).CountAsync() },
        new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => EF.Functions.DateDiffDay(s.CreateDate, now) > 90).CountAsync() }
    };
    return WebResponseContent.Instance.OK(null, result);
}
```
- [ ] **Step 7: å®žçް StockByWarehouse æŽ¥å£**
```csharp
public async Task<WebResponseContent> StockByWarehouse()
{
    var result = await _db.Queryable<Dt_StockInfo>()
        .GroupBy(s => s.WarehouseId)
        .Select(g => new
        {
            WarehouseId = g.Key,
            Count = g.Count()
        })
        .ToListAsync();
    // è”查仓库名称
    var warehouseIds = result.Select(x => x.WarehouseId).ToList();
    var warehouses = await _db.Queryable<Dt_Warehouse>()
        .Where(w => warehouseIds.Contains(w.WarehouseId))
        .Select(w => new { w.WarehouseId, w.WarehouseName })
        .ToListAsync();
    var finalResult = result.Select(r =>
    {
        var wh = warehouses.FirstOrDefault(w => w.WarehouseId == r.WarehouseId);
        return new
        {
            Warehouse = wh?.WarehouseName ?? $"仓库{r.WarehouseId}",
            Count = r.Count
        };
    }).ToList();
    return WebResponseContent.Instance.OK(null, finalResult);
}
```
- [ ] **Step 8: æäº¤ä»£ç **
```bash
git add WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
git commit -m "feat(Dashboard): æ·»åŠ ä»ªè¡¨ç›˜æŽ§åˆ¶å™¨ï¼ŒåŒ…å«6个统计接口"
```
---
### Task 2: é‡å†™å‰ç«¯ Home.vue å®žçŽ°ä»ªè¡¨ç›˜
**文件:**
- Modify: `WIDESEA_WMSClient/src/views/Home.vue`
**说明:** é‡å†™ä¸ºç©ºç™½çš„首页,实现仪表盘图表布局
- [ ] **Step 1: é‡å†™ Home.vue æ¨¡æ¿éƒ¨åˆ†**
```vue
<template>
  <div class="dashboard-container">
    <!-- é¡¶éƒ¨ï¼šæœ¬æœˆå‡ºå…¥åº“趋势 (全宽) -->
    <div class="chart-row full-width">
      <div class="chart-card">
        <div class="card-title">本月出入库趋势</div>
        <div id="chart-monthly-trend" class="chart-content"></div>
      </div>
    </div>
    <!-- ç¬¬äºŒè¡Œï¼šä»Šæ—¥/本周出入库对比 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">今日出入库对比</div>
        <div id="chart-today" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">本周出入库对比</div>
        <div id="chart-week" class="chart-content"></div>
      </div>
    </div>
    <!-- ç¬¬ä¸‰è¡Œï¼šæœ¬æœˆå¯¹æ¯”/库存总量 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">本月出入库对比</div>
        <div id="chart-month" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">当前库存总量</div>
        <div class="stock-total">
          <div class="total-number">{{ overviewData.TotalStock || 0 }}</div>
          <div class="total-label">托盘</div>
        </div>
      </div>
    </div>
    <!-- ç¬¬å››è¡Œï¼šåº“龄分布/仓库分布 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">库存库龄分布</div>
        <div id="chart-stock-age" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">各仓库库存分布</div>
        <div id="chart-warehouse" class="chart-content"></div>
      </div>
    </div>
  </div>
</template>
```
- [ ] **Step 2: é‡å†™è„šæœ¬éƒ¨åˆ†**
```javascript
<script>
import * as echarts from "echarts";
export default {
  name: "Home",
  data() {
    return {
      charts: {},
      overviewData: {
        TodayInbound: 0,
        TodayOutbound: 0,
        MonthInbound: 0,
        MonthOutbound: 0,
        TotalStock: 0
      },
      weeklyData: [],
      monthlyData: [],
      stockAgeData: [],
      warehouseData: []
    };
  },
  mounted() {
    this.initCharts();
    this.loadData();
    window.addEventListener("resize", this.handleResize);
  },
  beforeUnmount() {
    window.removeEventListener("resize", this.handleResize);
    Object.values(this.charts).forEach(chart => chart.dispose());
  },
  methods: {
    handleResize() {
      Object.values(this.charts).forEach(chart => chart.resize());
    },
    initCharts() {
      this.charts.monthlyTrend = echarts.init(document.getElementById("chart-monthly-trend"));
      this.charts.today = echarts.init(document.getElementById("chart-today"));
      this.charts.week = echarts.init(document.getElementById("chart-week"));
      this.charts.month = echarts.init(document.getElementById("chart-month"));
      this.charts.stockAge = echarts.init(document.getElementById("chart-stock-age"));
      this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse"));
    },
    async loadData() {
      await this.loadOverview();
      await this.loadWeeklyStats();
      await this.loadMonthlyStats();
      await this.loadStockAgeDistribution();
      await this.loadStockByWarehouse();
    },
    async loadOverview() {
      try {
        const res = await this.http.get("/api/Dashboard/Overview");
        if (res.Status && res.Data) {
          this.overviewData = res.Data;
          this.updateTodayChart();
          this.updateWeekChart();
          this.updateMonthChart();
        }
      } catch (e) {
        console.error("加载总览数据失败", e);
      }
    },
    async loadWeeklyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 });
        if (res.Status && res.Data) {
          this.weeklyData = res.Data;
          this.updateWeekChart();
        }
      } catch (e) {
        console.error("加载每周统计失败", e);
      }
    },
    async loadMonthlyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/MonthlyStats", { months: 12 });
        if (res.Status && res.Data) {
          this.monthlyData = res.Data;
          this.updateMonthlyTrendChart();
        }
      } catch (e) {
        console.error("加载每月统计失败", e);
      }
    },
    async loadStockAgeDistribution() {
      try {
        const res = await this.http.get("/api/Dashboard/StockAgeDistribution");
        if (res.Status && res.Data) {
          this.stockAgeData = res.Data;
          this.updateStockAgeChart();
        }
      } catch (e) {
        console.error("加载库龄分布失败", e);
      }
    },
    async loadStockByWarehouse() {
      try {
        const res = await this.http.get("/api/Dashboard/StockByWarehouse");
        if (res.Status && res.Data) {
          this.warehouseData = res.Data;
          this.updateWarehouseChart();
        }
      } catch (e) {
        console.error("加载仓库分布失败", e);
      }
    },
    // æ›´æ–°ä»Šæ—¥å¯¹æ¯”图表
    updateTodayChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["今日"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.TodayInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.TodayOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.today.setOption(option, true);
    },
    // æ›´æ–°æœ¬å‘¨å¯¹æ¯”图表
    updateWeekChart() {
      // æœ¬å‘¨æ•°æ®ä»Ž weeklyData ä¸­è®¡ç®—当周数据
      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 month = monday.getMonth() + 1;
      const day = monday.getDate();
      // ISO week start (Monday)
      const jan1 = new Date(year, 0, 1);
      const weekNum = Math.ceil(((monday - jan1) / 86400000 + jan1.getDay() + 1) / 7);
      return `${year}-W${String(weekNum).padStart(2, "0")}`;
    },
    // æ›´æ–°æœ¬æœˆå¯¹æ¯”图表
    updateMonthChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本月"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.MonthInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.MonthOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.month.setOption(option, true);
    },
    // æ›´æ–°æœˆåº¦è¶‹åŠ¿å›¾è¡¨ï¼ˆæŠ˜çº¿å›¾ï¼‰
    updateMonthlyTrendChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.monthlyData.map(m => m.Month),
          axisLabel: { color: "#fff", rotate: 45 }
        },
        yAxis: [
          {
            type: "value",
            name: "数量",
            axisLabel: { color: "#fff" }
          }
        ],
        series: [
          { name: "入库", type: "line", data: this.monthlyData.map(m => m.Inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "line", data: this.monthlyData.map(m => m.Outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.monthlyTrend.setOption(option, true);
    },
    // æ›´æ–°åº“龄分布图表
    updateStockAgeChart() {
      const option = {
        tooltip: { trigger: "item" },
        legend: { data: this.stockAgeData.map(s => s.Range), textStyle: { color: "#fff" } },
        series: [
          {
            type: "pie",
            radius: "60%",
            data: this.stockAgeData.map((s, i) => ({
              name: s.Range,
              value: s.Count
            })),
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: "rgba(0, 0, 0, 0.5)"
              }
            }
          }
        ]
      };
      this.charts.stockAge.setOption(option, true);
    },
    // æ›´æ–°ä»“库分布图表
    updateWarehouseChart() {
      const option = {
        tooltip: { trigger: "axis" },
        xAxis: {
          type: "category",
          data: this.warehouseData.map(w => w.Warehouse),
          axisLabel: { color: "#fff", rotate: 30 }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          {
            type: "bar",
            data: this.warehouseData.map(w => w.Count),
            itemStyle: { color: "#5470c6" }
          }
        ]
      };
      this.charts.warehouse.setOption(option, true);
    }
  }
};
</script>
```
- [ ] **Step 3: æ·»åŠ æ ·å¼**
```vue
<style scoped>
.dashboard-container {
  padding: 20px;
  background-color: #0e1a2b;
  min-height: calc(100vh - 60px);
}
.chart-row {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}
.chart-row.full-width {
  width: 100%;
}
.chart-card {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(25, 186, 139, 0.17);
  border-radius: 4px;
  padding: 15px;
  position: relative;
}
.chart-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-left: 2px solid #02a6b5;
}
.chart-card::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-right: 2px solid #02a6b5;
}
.card-title {
  color: #fff;
  font-size: 16px;
  text-align: center;
  margin-bottom: 10px;
}
.chart-content {
  height: 280px;
  width: 100%;
}
.stock-total {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 280px;
}
.total-number {
  font-size: 64px;
  font-weight: bold;
  color: #67caca;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
}
.total-label {
  font-size: 18px;
  color: #fcf0d8;
  margin-top: 10px;
}
/* å…¨å®½å›¾è¡¨ */
.full-width .chart-card {
  flex: none;
  width: 100%;
}
.full-width .chart-content {
  height: 350px;
}
</style>
```
- [ ] **Step 4: æäº¤ä»£ç **
```bash
git add WIDESEA_WMSClient/src/views/Home.vue
git commit -m "feat(Home): é‡å†™é¦–页为仪表盘图表页面"
```
---
### Task 3: éªŒè¯å®žçް
- [ ] **Step 1: æž„建后端**
```bash
cd WIDESEA_WMSServer
dotnet build WIDESEA_WMSServer.sln
```
预期:构建成功,无错误
- [ ] **Step 2: æž„建前端**
```bash
cd WIDESEA_WMSClient
yarn build
```
预期:构建成功,无错误
- [ ] **Step 3: å¯åŠ¨åŽç«¯æµ‹è¯• API**
```bash
cd WIDESEA_WMSServer/WIDESEA_WMSServer
dotnet run
```
使用浏览器或 Postman æµ‹è¯•:
- `GET http://localhost:9291/api/Dashboard/Overview`
- `GET http://localhost:9291/api/Dashboard/DailyStats?days=30`
- `GET http://localhost:9291/api/Dashboard/WeeklyStats?weeks=12`
- `GET http://localhost:9291/api/Dashboard/MonthlyStats?months=12`
- `GET http://localhost:9291/api/Dashboard/StockAgeDistribution`
- `GET http://localhost:9291/api/Dashboard/StockByWarehouse`
预期:各接口返回 JSON æ•°æ®ï¼Œæ ¼å¼ç¬¦åˆè®¾è®¡æ–‡æ¡£
---
## æ€»ç»“
### åŽç«¯ï¼ˆDashboardController)
| æŽ¥å£ | è·¯ç”± | è¯´æ˜Ž |
|------|------|------|
| Overview | GET /api/Dashboard/Overview | æ€»è§ˆæ•°æ® |
| DailyStats | GET /api/Dashboard/DailyStats?days=30 | æ¯æ—¥ç»Ÿè®¡ |
| WeeklyStats | GET /api/Dashboard/WeeklyStats?weeks=12 | æ¯å‘¨ç»Ÿè®¡ |
| MonthlyStats | GET /api/Dashboard/MonthlyStats?months=12 | æ¯æœˆç»Ÿè®¡ |
| StockAgeDistribution | GET /api/Dashboard/StockAgeDistribution | åº“龄分布 |
| StockByWarehouse | GET /api/Dashboard/StockByWarehouse | ä»“库分布 |
### å‰ç«¯ï¼ˆHome.vue)
| å›¾è¡¨ | ç»„ä»¶ ID | å›¾è¡¨ç±»åž‹ |
|------|---------|----------|
| æœ¬æœˆå‡ºå…¥åº“趋势 | chart-monthly-trend | æŠ˜çº¿å›¾ |
| ä»Šæ—¥å‡ºå…¥åº“对比 | chart-today | æŸ±çж图 |
| æœ¬å‘¨å‡ºå…¥åº“对比 | chart-week | æŸ±çж图 |
| æœ¬æœˆå‡ºå…¥åº“对比 | chart-month | æŸ±çж图 |
| å½“前库存总量 | (数字卡片) | - |
| åº“存库龄分布 | chart-stock-age | é¥¼å›¾ |
| å„仓库库存分布 | chart-warehouse | æŸ±çж图 |
Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,314 @@
# åº“å­˜3D查看器 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
**Goal:** å®žçŽ°åº“å­˜3D查看器,用户可在 Three.js 3D åœºæ™¯ä¸­å·¡è§†ä»“库、点击货位查看库存详情
**Architecture:** å‰ç«¯ Vue 3 + Element Plus + Three.js,后端 ASP.NET Core 6 Web API + SignalR å®žæ—¶æŽ¨é€
**Tech Stack:** Three.js, @microsoft/signalr, Element Plus, Vue 3 Composition API
---
## æ–‡ä»¶ç»“æž„
```
后端 (WIDESEA_WMSServer)
├── WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs      # 3D布局响应DTO [新建]
├── WIDESEA_IStockService/IStockInfoService.cs  # æ·»åŠ Get3DLayoutAsync方法签名 [修改]
├── WIDESEA_StockService/StockInfoService.cs    # å®žçްGet3DLayoutAsync [修改]
├── WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs  # æ·»åŠ Get3DLayout端点 [修改]
└── WIDESEA_WMSServer/Hubs/StockHub.cs         # SignalR Hub [新建]
前端 (WIDESEA_WMSClient)
├── package.json                                # æ·»åŠ three依赖 [修改]
├── src/router/viewGird.js                      # æ³¨å†Œè·¯ç”± [修改]
├── src/views/stock/stockChat.vue               # ä¸»é¡µé¢ç»„ä»¶ [新建]
└── src/extension/stock/stockChat.js            # æ‰©å±•配置 [新建]
```
---
## å®žçŽ°ä»»åŠ¡
### Task 1: åŽç«¯ - åˆ›å»º Stock3DLayoutDTO
**Files:**
- Create: `WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs`
**详细规范:**
创建两个 DTO ç±»ï¼š
1. `Stock3DLayoutDTO` - åŒ…含仓库基本信息、尺寸、筛选列表、货位数组
2. `Location3DItemDTO` - åŒ…含单个货位的所有3D渲染所需数据
**验收标准:**
- DTO åŒ…含所有 spec ä¸­å®šä¹‰çš„字段
- å‘½åç©ºé—´æ­£ç¡®
- å¯ä»¥è¢« Service å±‚正确引用
```csharp
namespace WIDESEA_DTO.Stock
{
    /// <summary>
    /// ä»“库3D布局响应DTO
    /// </summary>
    public class Stock3DLayoutDTO
    {
        public int WarehouseId { get; set; }
        public string WarehouseName { get; set; }
        public int MaxRow { get; set; }
        public int MaxColumn { get; set; }
        public int MaxLayer { get; set; }
        public List<string> MaterielCodeList { get; set; } = new();
        public List<string> BatchNoList { get; set; } = new();
        public List<Location3DItemDTO> Locations { get; set; } = new();
    }
    /// <summary>
    /// è´§ä½3D数据项
    /// </summary>
    public class Location3DItemDTO
    {
        public int LocationId { get; set; }
        public string LocationCode { get; set; }
        public int Row { get; set; }
        public int Column { get; set; }
        public int Layer { get; set; }
        public int LocationStatus { get; set; } // 0=空, 1=占用, 2=锁定, 3=禁用
        public int StockStatus { get; set; } // 0=无货, 1=有货, 2=库存紧张, 3=已满
        public float StockQuantity { get; set; }
        public float MaxCapacity { get; set; }
        public string? PalletCode { get; set; }
        public string? MaterielCode { get; set; }
        public string? MaterielName { get; set; }
        public string? BatchNo { get; set; }
    }
}
```
---
### Task 2: åŽç«¯ - æ›´æ–° IStockInfoService æŽ¥å£
**Files:**
- Modify: `WIDESEA_WMSServer/WIDESEA_IStockService/IStockInfoService.cs`
**详细规范:**
在接口中添加方法签名:
```csharp
/// <summary>
/// èŽ·å–ä»“åº“3D布局数据
/// </summary>
/// <param name="warehouseId">仓库ID</param>
/// <returns>3D布局DTO</returns>
Task<Stock3DLayoutDTO> Get3DLayoutAsync(int warehouseId);
```
**验收标准:**
- æ–¹æ³•签名正确
- æ·»åŠ äº†æ–‡æ¡£æ³¨é‡Š
- å¼•用了 Stock3DLayoutDTO
---
### Task 3: åŽç«¯ - å®žçް Get3DLayoutAsync
**Files:**
- Modify: `WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs`
**详细规范:**
实现 Get3DLayoutAsync æ–¹æ³•:
1. æŸ¥è¯¢ä»“库信息
2. æŸ¥è¯¢è¯¥ä»“库所有货位
3. æŸ¥è¯¢åº“存信息(包含明细)
4. æå–物料编号和批次号列表
5. æ˜ å°„到 Location3DItemDTO
6. è®¡ç®—仓库尺寸
**验收标准:**
- æ–¹æ³•能正确返回 Stock3DLayoutDTO
- æ‰€æœ‰ locationStatus å’Œ stockStatus å€¼æ­£ç¡®æ˜ å°„
- æ€§èƒ½é€‚合中型仓库(1000-5000货位)
---
### Task 4: åŽç«¯ - æ·»åŠ  API ç«¯ç‚¹
**Files:**
- Modify: `WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs`
**详细规范:**
添加端点:
```csharp
/// <summary>
/// èŽ·å–ä»“åº“3D布局
/// </summary>
/// <param name="warehouseId">仓库ID</param>
/// <returns>3D布局数据</returns>
[HttpGet("Get3DLayout")]
public async Task<WebResponseContent> Get3DLayout(int warehouseId)
{
    var result = await Service.Get3DLayoutAsync(warehouseId);
    return WebResponseContent.Instance.OK(result);
}
```
**验收标准:**
- è·¯ç”±æ­£ç¡®ï¼šGET /api/StockInfo/Get3DLayout?warehouseId={id}
- è¿”回格式符合 WebResponseContent è§„范
---
### Task 5: åŽç«¯ - åˆ›å»º SignalR Hub
**Files:**
- Create: `WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs`
- Modify: `WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs`
**详细规范:**
1. åˆ›å»º StockHub ç±»ï¼Œç»§æ‰¿ Microsoft.AspNetCore.SignalR.Hub
2. æ·»åŠ  SendStockUpdate æ–¹æ³•供外部调用
3. åœ¨ Program.cs ä¸­æ³¨å†Œ SignalR æœåŠ¡å¹¶æ˜ å°„ Hub
**验收标准:**
- Hub å¯è¢«å‰ç«¯è¿žæŽ¥
- SendStockUpdate æ–¹æ³•存在且可被调用
---
### Task 6: å‰ç«¯ - å®‰è£… Three.js ä¾èµ–
**Files:**
- Modify: `WIDESEA_WMSClient/package.json`
**详细规范:**
添加 three ä¾èµ–到 package.json:
```json
"three": "^0.160.0"
```
**验收标准:**
- package.json åŒ…含 three ä¾èµ–
- ç‰ˆæœ¬å·åˆç†ï¼ˆ^0.160.0 æˆ–更新稳定版)
---
### Task 7: å‰ç«¯ - æ³¨å†Œè·¯ç”±
**Files:**
- Modify: `WIDESEA_WMSClient/src/router/viewGird.js`
**详细规范:**
在 stockView è·¯ç”±åŽæ·»åŠ ï¼š
```javascript
{
  path: '/stockChat',
  name: 'stockChat',
  component: () => import('@/views/stock/stockChat.vue')
}
```
**验收标准:**
- è·¯ç”±æ³¨å†Œæ­£ç¡®
- ä¸Žå…¶ä»–路由格式一致
---
### Task 8: å‰ç«¯ - åˆ›å»º stockChat.vue ä¸»ç»„ä»¶
**Files:**
- Create: `WIDESEA_WMSClient/src/views/stock/stockChat.vue`
**详细规范:**
组件必须包含:
1. ä»“库 Tabs(el-tabs)
2. å·¥å…·æ ï¼ˆç­›é€‰ + é‡ç½®è§†è§’按钮)
3. 3D Canvas å®¹å™¨
4. çŠ¶æ€å›¾ä¾‹
5. è¯¦æƒ…弹窗(el-dialog fullscreen)
Three.js åœºæ™¯ï¼š
1. åœºæ™¯åˆå§‹åŒ–(背景色 0x1a1a2e)
2. é€è§†ç›¸æœº
3. WebGLRenderer
4. OrbitControls(阻尼启用的轨道控制器)
5. çŽ¯å¢ƒå…‰ + å®šå‘å…‰
6. åœ°é¢ï¼ˆPlaneGeometry,网格)
7. InstancedMesh æ‰¹é‡æ¸²æŸ“货位
8. Raycaster ç‚¹å‡»æ‹¾å–
9. ç›¸æœº lerp èšç„¦åŠ¨ç”»
颜色编码(前端实现):
- DISABLED(3): 0x2d2d2d
- LOCKED(2): 0xF56C6C
- EMPTY(0/无货): 0x4a4a4a
- HAS_STOCK(1): 0x409EFF
- LOW_STOCK(2): 0xE6A23C
- FULL(3): 0x67C23A
**验收标准:**
- é¡µé¢å¯ä»¥æ­£å¸¸åŠ è½½
- Three.js åœºæ™¯æ­£ç¡®åˆå§‹åŒ–
- ç‚¹å‡»è´§ä½èƒ½æ˜¾ç¤ºè¯¦æƒ…弹窗
- é¢œè‰²ç¼–码正确
---
### Task 9: å‰ç«¯ - åˆ›å»ºæ‰©å±•配置文件
**Files:**
- Create: `WIDESEA_WMSClient/src/extension/stock/stockChat.js`
**详细规范:**
创建标准扩展文件格式:
```javascript
let extension = {
  components: {
    gridHeader: '',
    gridBody: '',
    gridFooter: '',
    modelHeader: '',
    modelBody: '',
    modelFooter: ''
  },
  tableAction: '',
  buttons: { view: [], box: [], detail: [] },
  methods: {
    onInit() {},
    onInited() {}
  }
};
export default extension;
```
**验收标准:**
- ç¬¦åˆé¡¹ç›®çŽ°æœ‰æ‰©å±•æ–‡ä»¶æ¨¡å¼
---
### Task 10: å‰ç«¯ - é›†æˆ SignalR å®žæ—¶æ›´æ–°
**Files:**
- Modify: `WIDESEA_WMSClient/src/views/stock/stockChat.vue`
**详细规范:**
1. åœ¨ onMounted ä¸­åˆå§‹åŒ– SignalR è¿žæŽ¥
2. è¿žæŽ¥ /stockHub
3. ç›‘听 StockUpdated äº‹ä»¶
4. æ›´æ–°å¯¹åº”货位的 stockQuantity å’Œ stockStatus
5. åŠ¨æ€æ›´æ–°è´§ä½é¢œè‰²
6. åœ¨ onUnmounted ä¸­æ–­å¼€è¿žæŽ¥
**验收标准:**
- SignalR è¿žæŽ¥æ­£å¸¸å»ºç«‹
- æ”¶åˆ°æ›´æ–°æ—¶è´§ä½é¢œè‰²èƒ½åŠ¨æ€å˜åŒ–
Code/WMS/docs/superpowers/plans/2026-03-30-ÈÎÎñ¿â´æÀúÊ·¼Ç¼ʵÏּƻ®.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,273 @@
# ä»»åŠ¡åº“å­˜åŽ†å²è®°å½•å®žçŽ°è®¡åˆ’
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** åœ¨ TaskService çš„任务完成方法中添加任务历史和库存历史保存。
**Architecture:** æ³¨å…¥ `ITask_HtyService` å’Œ `IStockInfo_HtyService`,修改 `CompleteTaskAsync` æ·»åŠ åŽ†å²ä¿å­˜ï¼Œä¸¤ä¸ªç©ºæ‰˜ç›˜æ–¹æ³•å†…è”æ·»åŠ åŽ†å²ä¿å­˜ã€‚
**Tech Stack:** ASP.NET Core 6.0, MapsterMapper, SqlSugar
---
## ä»»åŠ¡æ€»è§ˆ
| ä»»åŠ¡ | å†…容 |
|------|------|
| Task 1 | æ³¨å…¥æœåŠ¡ï¼ˆITask_HtyService, IStockInfo_HtyService) |
| Task 2 | ä¿®æ”¹ CompleteTaskAsync æ·»åŠ ä»»åŠ¡åŽ†å²ä¿å­˜ |
| Task 3 | ä¿®æ”¹ 3 ä¸ªè°ƒç”¨æ–¹ä¼ å…¥ operateType |
| Task 4 | InboundFinishTaskTrayAsync æ·»åŠ åŽ†å²ä¿å­˜ |
| Task 5 | OutboundFinishTaskTrayAsync æ·»åŠ åŽ†å²ä¿å­˜ |
---
## Task 1: æ³¨å…¥åŽ†å²æœåŠ¡
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`
- [ ] **Step 1: æ·»åŠ å­—æ®µ**
在 `_mesService` å­—段后添加:
```csharp
private readonly ITask_HtyService _task_HtyService;
private readonly IStockInfo_HtyService _stockInfo_HtyService;
```
- [ ] **Step 2: ä¿®æ”¹æž„造函数**
在构造函数参数中添加:
```csharp
ITask_HtyService task_HtyService,
IStockInfo_HtyService stockInfo_HtyService,
```
在构造函数体内添加:
```csharp
_task_HtyService = task_HtyService;
_stockInfo_HtyService = stockInfo_HtyService;
```
- [ ] **Step 3: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
- [ ] **Step 4: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): æ³¨å…¥ITask_HtyService和IStockInfo_HtyService"
```
---
## Task 2: ä¿®æ”¹ CompleteTaskAsync æ·»åŠ ä»»åŠ¡åŽ†å²ä¿å­˜
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`(`CompleteTaskAsync` æ–¹æ³•)
- [ ] **Step 1: ä¿®æ”¹æ–¹æ³•签名**
将:
```csharp
private async Task<WebResponseContent> CompleteTaskAsync(Dt_Task task)
```
修改为:
```csharp
private async Task<WebResponseContent> CompleteTaskAsync(Dt_Task task, string operateType)
```
- [ ] **Step 2: ä¿®æ”¹åŽ†å²ä¿å­˜é€»è¾‘**
将:
```csharp
var historyTask = _mapper.Map<Dt_Task_Hty>(task);
historyTask.InsertTime = DateTime.Now;
```
修改为:
```csharp
var historyTask = _mapper.Map<Dt_Task_Hty>(task);
historyTask.InsertTime = DateTime.Now;
historyTask.OperateType = operateType;
var saveResult = await _task_HtyService.Repository.AddDataAsync(historyTask) > 0;
if (!saveResult) return WebResponseContent.Instance.Error("任务历史保存失败");
```
- [ ] **Step 3: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
- [ ] **Step 4: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): CompleteTaskAsync添加任务历史保存逻辑"
```
---
## Task 3: ä¿®æ”¹ 3 ä¸ªè°ƒç”¨æ–¹ä¼ å…¥ operateType
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`
- [ ] **Step 1: ä¿®æ”¹ InboundFinishTaskAsync çš„ CompleteTaskAsync è°ƒç”¨**
将:
```csharp
return await CompleteTaskAsync(task);
```
修改为:
```csharp
return await CompleteTaskAsync(task, "入库完成");
```
- [ ] **Step 2: ä¿®æ”¹ OutboundFinishTaskAsync çš„ CompleteTaskAsync è°ƒç”¨**
将:
```csharp
return await CompleteTaskAsync(task);
```
修改为:
```csharp
return await CompleteTaskAsync(task, "出库完成");
```
- [ ] **Step 3: ä¿®æ”¹ RelocationFinishTaskAsync çš„ CompleteTaskAsync è°ƒç”¨**
将:
```csharp
return await CompleteTaskAsync(task);
```
修改为:
```csharp
return await CompleteTaskAsync(task, "移库完成");
```
- [ ] **Step 4: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
- [ ] **Step 5: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): ä»»åŠ¡å®Œæˆæ–¹æ³•ä¼ å…¥æ­£ç¡®çš„OperateType"
```
---
## Task 4: InboundFinishTaskTrayAsync æ·»åŠ åŽ†å²ä¿å­˜
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`(`InboundFinishTaskTrayAsync` æ–¹æ³•,约第 403 è¡Œï¼‰
- [ ] **Step 1: åœ¨åˆ é™¤ä»»åŠ¡å‰æ·»åŠ ä»»åŠ¡åŽ†å²å’Œåº“å­˜åŽ†å²ä¿å­˜**
在 `var deleteResult = await BaseDal.DeleteDataAsync(task);` ä¹‹å‰æ·»åŠ ï¼š
```csharp
// ä¿å­˜ä»»åŠ¡åŽ†å²
var historyTask = _mapper.Map<Dt_Task_Hty>(task);
historyTask.InsertTime = DateTime.Now;
historyTask.OperateType = "空托盘入库完成";
if (await _task_HtyService.Repository.AddDataAsync(historyTask) <= 0)
    return content.Error("任务历史保存失败");
// ä¿å­˜åº“存历史
var historyStock = _mapper.Map<Dt_StockInfo_Hty>(stockInfo);
historyStock.InsertTime = DateTime.Now;
historyStock.OperateType = "空托盘入库完成";
if (await _stockInfo_HtyService.Repository.AddDataAsync(historyStock) <= 0)
    return content.Error("库存历史保存失败");
```
- [ ] **Step 2: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
- [ ] **Step 3: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): InboundFinishTaskTrayAsync添加任务和库存历史保存"
```
---
## Task 5: OutboundFinishTaskTrayAsync æ·»åŠ åŽ†å²ä¿å­˜
**Files:**
- Modify: `WIDESEA_TaskInfoService/TaskService.cs`(`OutboundFinishTaskTrayAsync` æ–¹æ³•,约第 487 è¡Œï¼‰
- [ ] **Step 1: åœ¨åˆ é™¤ä»»åŠ¡å‰æ·»åŠ ä»»åŠ¡åŽ†å²å’Œåº“å­˜åŽ†å²ä¿å­˜**
在 `var deleteResult = await BaseDal.DeleteDataAsync(task);` ä¹‹å‰æ·»åŠ ï¼š
```csharp
// ä¿å­˜ä»»åŠ¡åŽ†å²
var historyTask = _mapper.Map<Dt_Task_Hty>(task);
historyTask.InsertTime = DateTime.Now;
historyTask.OperateType = "空托盘出库完成";
if (await _task_HtyService.Repository.AddDataAsync(historyTask) <= 0)
    return content.Error("任务历史保存失败");
// ä¿å­˜åº“存历史
var historyStock = _mapper.Map<Dt_StockInfo_Hty>(stockInfo);
historyStock.InsertTime = DateTime.Now;
historyStock.OperateType = "空托盘出库完成";
if (await _stockInfo_HtyService.Repository.AddDataAsync(historyStock) <= 0)
    return content.Error("库存历史保存失败");
```
- [ ] **Step 2: æž„建验证**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
```
- [ ] **Step 3: æäº¤**
```bash
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat(TaskService): OutboundFinishTaskTrayAsync添加任务和库存历史保存"
```
---
## Task 6: æ•´ä½“构建验证
- [ ] **Step 1: æž„建整个解决方案**
```bash
cd WIDESEA_WMSServer && dotnet build WIDESEA_WMSServer.sln
```
确认无编译错误。
---
## éªŒè¯æ£€æŸ¥æ¸…单
- [ ] `ITask_HtyService` å’Œ `IStockInfo_HtyService` å·²æ³¨å…¥
- [ ] `CompleteTaskAsync` ç­¾åå·²ä¿®æ”¹ä¸ºå¸¦ `operateType` å‚æ•°
- [ ] `InboundFinishTaskAsync` ä¼ å…¥ `"入库完成"`
- [ ] `OutboundFinishTaskAsync` ä¼ å…¥ `"出库完成"`
- [ ] `RelocationFinishTaskAsync` ä¼ å…¥ `"移库完成"`
- [ ] `InboundFinishTaskTrayAsync` æ·»åŠ äº†ä»»åŠ¡åŽ†å²å’Œåº“å­˜åŽ†å²
- [ ] `OutboundFinishTaskTrayAsync` æ·»åŠ äº†ä»»åŠ¡åŽ†å²å’Œåº“å­˜åŽ†å²
- [ ] è§£å†³æ–¹æ¡ˆæž„建无错误
Code/WMS/docs/superpowers/specs/2026-03-30-MESÍÐÅ̽øÕ¾³öÕ¾¼¯³ÉÉè¼Æ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
# MES æ‰˜ç›˜è¿›ç«™å‡ºç«™é›†æˆè®¾è®¡
## æ¦‚è¿°
在 `TaskService` çš„入库完成/出库完成方法中集成 MES æ‰˜ç›˜è¿›ç«™/出站调用,新增空托盘入库/出库完成方法。
## äº‹åŠ¡ç­–ç•¥
MES è°ƒç”¨çº³å…¥ `ExecuteWithinTransactionAsync` å†… â€” MES å¤±è´¥æ—¶äº‹åŠ¡å›žæ»šï¼ŒWMS æ•°æ®ä¸è½åº“。
## æ•°æ®æµ
| æ–¹æ³• | æ“ä½œ | MES è°ƒç”¨ |
|------|------|----------|
| `InboundFinishTaskAsync` | å…¥åº“完成 | `InboundInContainer` |
| `OutboundFinishTaskAsync` | å‡ºåº“完成 | `OutboundInContainer` |
| `InboundFinishTaskTrayAsync`(新增) | ç©ºæ‰˜ç›˜å…¥åº“完成 | æ—  |
| `OutboundFinishTaskTrayAsync`(新增) | ç©ºæ‰˜ç›˜å‡ºåº“完成 | æ—  |
## æ¶‰åŠæ–‡ä»¶
- `WIDESEA_TaskInfoService/TaskService.cs`
## è¯¦ç»†è®¾è®¡
### 1. å…¥åº“完成 (InboundFinishTaskAsync)
**现有逻辑(事务内):**
- æ›´æ–°åº“存信息(LocationCode, OutboundDate, StockStatus)
- æ›´æ–°è´§ä½çŠ¶æ€ä¸º InStock
- è°ƒç”¨ CompleteTaskAsync åˆ é™¤ä»»åŠ¡
**修改后:**
- åœ¨ `CompleteTaskAsync` ä¹‹å‰æ·»åŠ  MES `InboundInContainer` è°ƒç”¨
- è¯·æ±‚参数:
  - `EquipmentCode = "STK-GROUP-001"`
  - `ResourceCode = "STK-GROUP-001"`
  - `ContainerCode = taskDto.PalletCode`
  - `LocalTime = DateTime.Now`
- MES å¤±è´¥ â†’ äº‹åŠ¡å›žæ»šï¼Œè¿”å›žé”™è¯¯
### 2. å‡ºåº“完成 (OutboundFinishTaskAsync)
**现有逻辑(事务内):**
- æ›´æ–°åº“存信息(LocationId=0, LocationCode=null, OutboundDate)
- æ›´æ–°è´§ä½çŠ¶æ€ä¸º Free
- è°ƒç”¨ CompleteTaskAsync åˆ é™¤ä»»åŠ¡
**修改后:**
- åœ¨ `CompleteTaskAsync` ä¹‹å‰æ·»åŠ  MES `OutboundInContainer` è°ƒç”¨
- è¯·æ±‚参数:
  - `EquipmentCode = "STK-GROUP-001"`
  - `ResourceCode = "STK-GROUP-001"`
  - `ContainerCode = taskDto.PalletCode`
  - `LocalTime = DateTime.Now`
- `OutboundInContainerRequest` æœ‰ `ParamList` å­—段,目前为空列表 `new List<ParamItem>()`
- MES å¤±è´¥ â†’ äº‹åŠ¡å›žæ»šï¼Œè¿”å›žé”™è¯¯
### 3. æ–°å¢žç©ºæ‰˜ç›˜å…¥åº“完成 (InboundFinishTaskTrayAsync)
**方法签名:**
```csharp
public async Task<WebResponseContent> InboundFinishTaskTrayAsync(CreateTaskDto taskDto)
```
**逻辑:**
1. æŸ¥è¯¢ä»»åŠ¡ï¼ˆæ‰˜ç›˜å· = taskDto.PalletCode)
2. æŸ¥è¯¢è´§ä½ä¿¡æ¯
3. æŸ¥è¯¢åº“存信息
4. äº‹åŠ¡å†…ï¼š
   - æ›´æ–°åº“å­˜ LocationCode/LocationId
   - æ›´æ–°è´§ä½çŠ¶æ€ä¸º InStock
   - æ›´æ–°åº“存状态为空托盘库存
   - åˆ é™¤ä»»åŠ¡ï¼ˆä¸è°ƒç”¨ CompleteTaskAsync,自己实现)
5. æ— éœ€ MES è°ƒç”¨
### 4. æ–°å¢žç©ºæ‰˜ç›˜å‡ºåº“完成 (OutboundFinishTaskTrayAsync)
**方法签名:**
```csharp
public async Task<WebResponseContent> OutboundFinishTaskTrayAsync(CreateTaskDto taskDto)
```
**逻辑:**
1. æŸ¥è¯¢ä»»åŠ¡ï¼ˆæ‰˜ç›˜å· = taskDto.PalletCode)
2. æŸ¥è¯¢è´§ä½ä¿¡æ¯
3. æŸ¥è¯¢åº“存信息
4. äº‹åŠ¡å†…ï¼š
   - æ›´æ–°åº“å­˜ LocationId=0, LocationCode=null
   - æ›´æ–°è´§ä½çŠ¶æ€ä¸º Free
   - æ›´æ–°åº“存状态为出库完成
   - åˆ é™¤ä»»åŠ¡ï¼ˆè‡ªå·±å®žçŽ°ï¼‰
5. æ— éœ€ MES è°ƒç”¨
## é”™è¯¯å¤„理
- MES è°ƒç”¨æ£€æŸ¥ `mesResult.Data?.IsSuccess`(MES ä¸šåŠ¡å±‚æˆåŠŸï¼‰
- é”™è¯¯è¿”回:`"任务完成失败:MES进站失败: {mesResult?.Data?.Msg ?? mesResult?.ErrorMessage ?? "未知错误"}"`
- ç©ºæ‰˜ç›˜æ–¹æ³•æ—  MES é”™è¯¯å¤„理
## è®¾å¤‡ç¼–码
统一使用 `STK-GROUP-001`,与组盘保持一致。
Code/WMS/docs/superpowers/specs/2026-03-30-MESµçо°ó¶¨½â°ó¼¯³ÉÉè¼Æ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
# MES ç”µèŠ¯ç»‘å®šè§£ç»‘é›†æˆè®¾è®¡
## æ¦‚è¿°
在 `StockService` çš„组盘、换盘、拆盘操作中,将 MES ç”µèŠ¯ç»‘å®š/解绑调用纳入 WMS äº‹åŠ¡å†…ï¼Œç¡®ä¿åº“å­˜æ•°æ®ä¸Ž MES çŠ¶æ€ä¸€è‡´ã€‚
## äº‹åŠ¡ç­–ç•¥
**MES è°ƒç”¨çº³å…¥ WMS äº‹åС内** â€” å¦‚æžœ MES è°ƒç”¨å¤±è´¥ï¼Œæ•´ä¸ªäº‹åŠ¡å›žæ»šï¼ŒWMS åº“存数据不会变化。
## æ•°æ®æµ
| æ“ä½œ | MES è°ƒç”¨ | æ—¶æœº |
|------|----------|------|
| **组盘** | `BindContainer` | WMS åº“存写入后 |
| **换盘** | `UnBindContainer` â†’ `BindContainer` | è§£ç»‘在换出前,绑定在换入后 |
| **拆盘** | `UnBindContainer` | åº“存明细删除前 |
## æ¶‰åŠæ–‡ä»¶
- `WIDESEA_StockService/StockSerivce.cs`
## è¯¦ç»†è®¾è®¡
### 1. ç»„盘 (GroupPalletAsync)
**现有逻辑:**
- åœ¨äº‹åŠ¡å†…æ‰§è¡Œ WMS åº“存写入
- ç¬¬ 166 è¡Œå·²å­˜åœ¨ `_mesService.BindContainer()` è°ƒç”¨ï¼Œä½†ç¼ºå°‘ `await` å’Œç»“果处理
**修改后:**
- ä¿®å¤ä¸º `await _mesService.BindContainer(bindRequest)`
- æ£€æŸ¥è¿”回结果,`result.Success == false` æ—¶äº‹åŠ¡å›žæ»š
- é”™è¯¯è¿”回:`"组盘成功,但MES调用失败: {MES错误}"`
### 2. æ¢ç›˜ (ChangePalletAsync)
**现有逻辑:**
- äº‹åŠ¡å†…ï¼šæŸ¥è¯¢æºæ‰˜ç›˜å’Œç›®æ ‡æ‰˜ç›˜ â†’ æ›´æ–°åº“存明细的 `StockId`
**修改后:**
- åœ¨æ›´æ–°åº“存明细前,调用 `UnBindContainer` è§£ç»‘源托盘电芯
- åœ¨æ›´æ–°åº“存明细后,调用 `BindContainer` ç»‘定到目标托盘
- MES å¤±è´¥ â†’ äº‹åŠ¡å›žæ»š
**UnBindContainer è¯·æ±‚构建:**
```csharp
var unbindRequest = new UnBindContainerRequest
{
    EquipmentCode = "STK-GROUP-001",
    ResourceCode = "STK-GROUP-001",
    LocalTime = DateTime.Now,
    ContainCode = stock.SourcePalletNo,
    SfcList = detailEntities.Select(d => d.SerialNumber).ToList()
};
```
### 3. æ‹†ç›˜ (SplitPalletAsync)
**现有逻辑:**
- äº‹åŠ¡å†…ï¼šæŸ¥è¯¢åº“å­˜æ˜Žç»† â†’ åˆ é™¤æ˜Žç»†è®°å½•
**修改后:**
- åˆ é™¤å‰ï¼Œè°ƒç”¨ `UnBindContainer` è§£ç»‘电芯
- MES å¤±è´¥ â†’ äº‹åŠ¡å›žæ»š
**UnBindContainer è¯·æ±‚构建:**
```csharp
var unbindRequest = new UnBindContainerRequest
{
    EquipmentCode = "STK-GROUP-001",
    ResourceCode = "STK-GROUP-001",
    LocalTime = DateTime.Now,
    ContainCode = stock.SourcePalletNo,
    SfcList = detailEntities.Select(d => d.SerialNumber).ToList()
};
```
## é”™è¯¯å¤„理
统一错误处理策略:
- MES è°ƒç”¨å¤±è´¥æ—¶ï¼Œäº‹åŠ¡å›žæ»š
- è¿”回:`"{操作}成功,但MES调用失败: {MES错误}"`
其中 `{MES错误}` æ¥è‡ª `result.Message`。
## è®¾å¤‡ç¼–码
硬编码 `EquipmentCode = "STK-GROUP-001"` å’Œ `ResourceCode = "STK-GROUP-001"`,与组盘现有逻辑保持一致。
## å®žçŽ°è¦ç‚¹
- æ‰€æœ‰ MES è°ƒç”¨ä½¿ç”¨ `await`
- æ£€æŸ¥ `HttpResponseResult<MesResponse>` çš„ `Success` å±žæ€§
- MES è°ƒç”¨å¤±è´¥æ—¶è¿”回错误信息,事务自动回滚(`ExecuteWithinTransactionAsync` åœ¨ `result.Status != true` æ—¶å›žæ»šï¼‰
Code/WMS/docs/superpowers/specs/2026-03-30-dashboard-chart-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,223 @@
# é¦–页仪表盘图表功能设计
## 1. æ¦‚è¿°
在 WMS å‰ç«¯é¦–页添加仪表盘图表,展示仓库的出入库统计和库存数据。数据基于任务完成时间进行统计。
> **数据来源说明:** å·²å®Œæˆä»»åŠ¡å­˜å‚¨åœ¨åŽ†å²è¡¨ `Dt_Task_Hty` ä¸­ï¼Œä½¿ç”¨ `InsertTime`(任务完成后移入历史的时间)作为任务完成时间。
## 2. é¡µé¢å¸ƒå±€
采用混合布局:顶部大图展示趋势,下方网格展示对比和详情。
```
┌─────────────────────────────────────────────────┐
│          æœ¬æœˆå‡ºå…¥åº“趋势(全宽,折线+柱状组合)      â”‚
├─────────────────────┬───────────────────────────┤
│    ä»Šæ—¥å‡ºå…¥åº“对比     â”‚      æœ¬å‘¨å‡ºå…¥åº“对比          â”‚
│    ï¼ˆæŸ±çŠ¶å›¾ï¼‰         â”‚      ï¼ˆæŸ±çŠ¶å›¾ï¼‰             â”‚
├─────────────────────┼───────────────────────────┤
│    æœ¬æœˆå‡ºå…¥åº“对比     â”‚      å½“前库存总量            â”‚
│    ï¼ˆæŸ±çŠ¶å›¾ï¼‰         â”‚      ï¼ˆæ•°å­—卡片)            â”‚
├─────────────────────┼───────────────────────────┤
│    åº“存库龄分布       â”‚      å„仓库库存分布          â”‚
│    ï¼ˆæŸ±çŠ¶å›¾ï¼‰         â”‚      ï¼ˆæŸ±çŠ¶å›¾ï¼‰              â”‚
└─────────────────────┴───────────────────────────┘
```
> ç¬¬ä¸€è¡Œï¼ˆæœ¬æœˆè¶‹åŠ¿å›¾ï¼‰ï¼šå…¨å®½æ˜¾ç¤º
> ä¸‹æ–¹ 2x2 ç½‘格:4个图表均匀分布
## 3. åŽç«¯æŽ¥å£è®¾è®¡
### 3.1 æ€»è§ˆæŽ¥å£
**GET** `/api/Dashboard/Overview`
返回首页加载时需要的所有汇总数字,一次调用获取关键指标。
**响应数据:**
```json
{
  "Status": true,
  "Data": {
    "TodayInbound": 120,
    "TodayOutbound": 95,
    "MonthInbound": 3500,
    "MonthOutbound": 3200,
    "TotalStock": 45000
  }
}
```
> æ³¨ï¼šåº“存数量单位为"托盘数"或"电芯数",取决于实际业务统计口径。
### 3.2 æ¯æ—¥ç»Ÿè®¡æŽ¥å£
**GET** `/api/Dashboard/DailyStats?days=30`
返回近N天的每日出入库统计。
**参数:**
- `days`:天数,默认 30,最大 365
- ç»Ÿè®¡åŸºäºŽæœåŠ¡å™¨æœ¬åœ°æ—¶é—´ï¼Œå½“æ—¥æ•°æ®åŒ…å«å½“æ—¥ç»Ÿè®¡
**响应数据:**
```json
{
  "Status": true,
  "Data": [
    { "Date": "2026-03-01", "Inbound": 120, "Outbound": 95 },
    { "Date": "2026-03-02", "Inbound": 150, "Outbound": 130 },
    ...
  ]
}
```
### 3.3 æ¯å‘¨ç»Ÿè®¡æŽ¥å£
**GET** `/api/Dashboard/WeeklyStats?weeks=12`
返回近N周的每周出入库统计。周从周一开始计算。
**参数:**
- `weeks`:周数,默认 12
**响应数据:**
```json
{
  "Status": true,
  "Data": [
    { "Week": "2026-W09", "Inbound": 850, "Outbound": 780 },
    { "Week": "2026-W10", "Inbound": 920, "Outbound": 870 },
    ...
  ]
}
```
### 3.4 æ¯æœˆç»Ÿè®¡æŽ¥å£
**GET** `/api/Dashboard/MonthlyStats?months=12`
返回近N月的每月出入库统计。
**参数:**
- `months`:月数,默认 12
**响应数据:**
```json
{
  "Status": true,
  "Data": [
    { "Month": "2025-04", "Inbound": 3500, "Outbound": 3200 },
    { "Month": "2025-05", "Inbound": 3800, "Outbound": 3600 },
    ...
  ]
}
```
### 3.5 åº“存库龄分布接口
**GET** `/api/Dashboard/StockAgeDistribution`
返回库存库龄分布数据。
**响应数据:**
```json
{
  "Status": true,
  "Data": [
    { "Range": "7天内", "Count": 12000 },
    { "Range": "7-30天", "Count": 18000 },
    { "Range": "30-90天", "Count": 10000 },
    { "Range": "90天以上", "Count": 5000 }
  ]
}
```
### 3.6 å„仓库库存分布接口
**GET** `/api/Dashboard/StockByWarehouse`
返回各仓库的库存数量分布。
**响应数据:**
```json
{
  "Status": true,
  "Data": [
    { "Warehouse": "仓库A", "Count": 15000 },
    { "Warehouse": "仓库B", "Count": 12000 },
    { "Warehouse": "仓库C", "Count": 18000 }
  ]
}
```
## 4. æŠ€æœ¯å®žçް
### 4.1 åŽç«¯å®žçް
**新增文件:**
- `WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs` - ä»ªè¡¨ç›˜æŽ§åˆ¶å™¨
**数据来源:**
- `Dt_Task_Hty` è¡¨ï¼šå·²å®Œæˆä»»åŠ¡çš„åŽ†å²è¡¨ï¼Œä½¿ç”¨ `InsertTime` ä½œä¸ºä»»åŠ¡å®Œæˆæ—¶é—´
- `TaskTypeEnum` æžšä¸¾ï¼šå…¥åº“任务范围 500-600(如 `Inbound=510`),出库任务范围 100-200(如 `Outbound=100`)
- `Dt_StockInfo` è¡¨ï¼šå½“前库存数据,使用 `CreateDate` ä½œä¸ºå…¥åº“时间计算库龄
**统计数据逻辑:**
- æŒ‰ `InsertTime` çš„æ—¥æœŸ/周/月分组统计任务数量
- å…¥åº“判断:`TaskType >= 500 && TaskType < 600`
- å‡ºåº“判断:`TaskType >= 100 && TaskType < 200`
- åº“龄 = å½“前时间 - `CreateDate`
### 4.2 å‰ç«¯å®žçް
**修改文件:**
- `WIDESEA_WMSClient/src/views/Home.vue` - é‡å†™ä¸ºç©ºç™½çš„首页,添加仪表盘图表
**图表组件:**
- å¤ç”¨ `src/views/charts/bigdata.vue` ä¸­çš„ ECharts ä½¿ç”¨æ¨¡å¼
- ä½¿ç”¨ ECharts 5.0.2
**页面结构:**
```
Home.vue
├── æœ¬æœˆè¶‹åŠ¿å›¾ï¼ˆæŠ˜çº¿+柱状组合)
├── ä»Šæ—¥/本周/本月出入库对比(柱状图)
├── å½“前库存总量(数字卡片)
├── åº“存库龄分布(柱状图)
└── å„仓库库存分布(柱状图)
```
**API è°ƒç”¨ï¼š**
- é¦–页加载时调用 Overview æŽ¥å£èŽ·å–æ±‡æ€»æ•°æ®
- å„图表组件 mounted æ—¶è°ƒç”¨å¯¹åº”接口获取详细数据
## 5. å®žçŽ°ä»»åŠ¡
### åŽç«¯
1. åˆ›å»º DashboardController
2. å®žçް Overview æŽ¥å£
3. å®žçް DailyStats æŽ¥å£
4. å®žçް WeeklyStats æŽ¥å£
5. å®žçް MonthlyStats æŽ¥å£
6. å®žçް StockAgeDistribution æŽ¥å£
7. å®žçް StockByWarehouse æŽ¥å£
### å‰ç«¯
1. é‡å†™ Home.vue,使用 ECharts å®žçŽ°ä»ªè¡¨ç›˜å¸ƒå±€
2. å®žçް Overview æŽ¥å£è°ƒç”¨
3. å®žçŽ°å„å›¾è¡¨ç»„ä»¶
4. è°ƒæ•´å›¾è¡¨æ ·å¼å’Œå“åº”式布局
## 6. é¢„计文件变更
**新增:**
- `WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs`
**修改:**
- `WIDESEA_WMSClient/src/views/Home.vue`
**参考:**
- `WIDESEA_WMSClient/src/views/charts/bigdata.vue` - ECharts ä½¿ç”¨ç¤ºä¾‹
Code/WMS/docs/superpowers/specs/2026-03-30-stock-chat-3d-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,258 @@
# åº“å­˜3D查看器 (stockChat) è®¾è®¡æ–‡æ¡£
## 1. æ¦‚è¿°
- **功能名称**:库存3D仓库查看器 (stockChat)
- **文件路径**:`WIDESEA_WMSClient/src/views/stock/stockChat.vue`
- **核心功能**:使用 Three.js + WebGL å®žçŽ°ä»“åº“3D可视化,用户可在3D场景中巡视仓库、查看货位状态、点击货位查看库存详情
- **目标用户**:仓库管理员、调度人员、质检人员
## 2. æŠ€æœ¯é€‰åž‹
| æŠ€æœ¯ | é€‰åž‹ | è¯´æ˜Ž |
|------|------|------|
| 3D引擎 | Three.js | WebGL ä¸»æµåº“,Vue 3 å‹å¥½ |
| æ¸²æŸ“ç­–ç•¥ | InstancedMesh | æ‰¹é‡æ¸²æŸ“大量货位,单次 drawcall |
| çŠ¶æ€ç®¡ç† | Vue 3 Composition API | `ref/reactive` |
| UI组件 | Element Plus | ä¸Žé¡¹ç›®çŽ°æœ‰æŠ€æœ¯æ ˆä¸€è‡´ |
| å®žæ—¶é€šä¿¡ | SignalR | ä¸ŽåŽç«¯ WebSocket é…åˆå®žçŽ°åº“å­˜åŠ¨æ€æ›´æ–° |
## 3. åŠŸèƒ½éœ€æ±‚
### 3.1 æ ¸å¿ƒåŠŸèƒ½
1. **仓库3D布局展示**
   - å…¨éƒ¨è´§ä½å‡æ¸²æŸ“,按状态着色
   - è´§æž¶å¼æŽ’列,包含巷道
   - åœ°é¢ç½‘格背景
2. **多仓库 Tab åˆ‡æ¢**
   - Element Plus Tabs ç»„ä»¶
   - æ¯ä¸ªä»“库独立加载数据
   - Tab åˆ‡æ¢æ—¶é‡ç½®3D场景
3. **货位点击详情弹窗**
   - å…¨å± Dialog å±•示
   - æ˜¾ç¤ºï¼šè´§ä½ä¿¡æ¯ã€åº“存状态、托盘编号、物料明细列表
   - æ”¯æŒæŸ¥çœ‹æ‰¹æ¬¡ã€ä¿è´¨æœŸé¢„è­¦
4. **3D场景交互**
   - é¼ æ ‡æ—‹è½¬ï¼ˆOrbitControls)
   - æ»šè½®ç¼©æ”¾
   - ä¸­é”®å¹³ç§»
   - ç‚¹å‡»è´§ä½é«˜äº® + ç›¸æœºèšç„¦
5. **实时库存状态更新**
   - SignalR ç›‘听库存变化
   - è´§ä½é¢œè‰²åŠ¨æ€æ›´æ–°
6. **筛选过滤**
   - æŒ‰ç‰©æ–™ç±»åž‹/批次/库存状态过滤
   - é«˜äº®æ˜¾ç¤ºåŒ¹é…è´§ä½
7. **状态图例**
   - å³ä¸Šè§’/底部颜色图例
   - è¯´æ˜Žæ¯ç§é¢œè‰²ä»£è¡¨çš„货位状态
### 3.2 è´§ä½çŠ¶æ€é¢œè‰²ç¼–ç ï¼ˆé¢œè‰²ä¼˜å…ˆçº§ï¼šlocationStatus > stockStatus)
**颜色判定规则(按优先级顺序):**
1. **`locationStatus = 3`(禁用)** â†’ æ·±ç° `#2d2d2d`(最高优先级)
2. **`locationStatus = 2`(锁定)** â†’ çº¢è‰² `#F56C6C`
3. **`locationStatus = 1`(占用)且 `stockStatus = 0`(无货)** â†’ æš—灰 `#4a4a4a`
4. **`locationStatus = 1`(占用)且 `stockStatus = 1`(有货)** â†’ è“è‰² `#409EFF`
5. **`locationStatus = 1`(占用)且 `stockStatus = 2`(库存紧张 <10%)** â†’ æ©™è‰² `#E6A23C`
6. **`locationStatus = 1`(占用)且 `stockStatus = 3`(已满 â‰¥90%)** â†’ ç»¿è‰² `#67C23A`
7. **`locationStatus = 0`(空)** â†’ æš—灰 `#4a4a4a`
**阈值定义:**
- åº“存紧张:`stockQuantity / maxCapacity < 10%`
- å·²æ»¡ï¼š`stockQuantity / maxCapacity â‰¥ 90%`
## 4. åŽç«¯ API
### 4.1 æ–°å¢žæŽ¥å£
```
GET /api/StockInfo/Get3DLayout?warehouseId={id}
```
**响应结构**:
```json
{
  "status": true,
  "data": {
    "warehouseId": 1,
    "warehouseName": "主仓库",
    "maxRow": 10,
    "maxColumn": 20,
    "maxLayer": 5,
    "materielCodeList": ["M001", "M002", "M003"],
    "batchNoList": ["B20260301", "B20260302"],
    "locations": [
      {
        "locationId": 1,
        "locationCode": "A-01-02-03",
        "row": 1,
        "column": 2,
        "layer": 3,
        "locationStatus": 0,
        "stockStatus": 2,
        "stockQuantity": 50,
        "maxCapacity": 100,
        "palletCode": "PLT-001",
        "materielCode": "M001",
        "materielName": "物料A",
        "batchNo": "B20260301"
      }
    ]
  }
}
```
**说明**:
- `locationStatus`: 0=空, 1=占用, 2=锁定, 3=禁用
- `stockStatus`: 0=无货, 1=有货, 2=库存紧张, 3=已满
- `maxCapacity`: è´§ä½æœ€å¤§å®¹é‡ï¼ˆç”¨äºŽè®¡ç®—填充率)
- `materielCodeList`: å½“前仓库所有物料编号列表(用于筛选下拉)
- `batchNoList`: å½“前仓库所有批次号列表(用于筛选下拉)
- **颜色判定在前端实现**:后端返回 `stockQuantity` å’Œ `maxCapacity`,前端按 3.2 è§„则计算颜色
### 4.2 SignalR å®žæ—¶æŽ¨é€
**Hub è·¯å¾„**:`/stockHub`
**推送事件**:
```javascript
// åº“存变化事件
stockUpdated: { locationId, warehouseId, stockQuantity, stockStatus }
```
## 5. å‰ç«¯æ–‡ä»¶ç»“æž„
```
WIDESEA_WMSClient/src/
├── views/stock/
│   â””── stockChat.vue              # ä¸»é¡µé¢ç»„ä»¶
├── extension/stock/
│   â””── stockChat.js               # ViewGrid æ‰©å±•配置
└── api/
    â””── http.js                    # å¤ç”¨çŽ°æœ‰ http å°è£…
```
## 6. ç»„件结构 (stockChat.vue)
```vue
<template>
  <div class="stock-chat-container">
    <!-- ä»“库 Tabs -->
    <el-tabs v-model="activeWarehouse" @tab-change="onWarehouseChange">
      <el-tab-pane
        v-for="wh in warehouseList"
        :key="wh.warehouseId"
        :label="wh.warehouseName"
        :name="wh.warehouseId"
      />
    </el-tabs>
    <!-- å·¥å…·æ  -->
    <div class="toolbar">
      <el-select v-model="filterStockStatus" placeholder="库存状态筛选" clearable>
        <el-option label="有货" :value="1" />
        <el-option label="库存紧张" :value="2" />
        <el-option label="已满" :value="3" />
      </el-select>
      <el-select v-model="filterMaterielCode" placeholder="物料筛选" clearable>
        <el-option v-for="code in materielCodeList" :key="code" :label="code" :value="code" />
      </el-select>
      <el-select v-model="filterBatchNo" placeholder="批次筛选" clearable>
        <el-option v-for="batch in batchNoList" :key="batch" :label="batch" :value="batch" />
      </el-select>
      <el-button @click="resetCamera">重置视角</el-button>
    </div>
    <!-- 3D Canvas -->
    <div ref="canvasContainer" class="canvas-container" />
    <!-- çŠ¶æ€å›¾ä¾‹ -->
    <div class="legend">
      <div v-for="item in legendItems" :key="item.status" class="legend-item">
        <span class="color-box" :style="{ background: item.color }" />
        <span>{{ item.label }}</span>
      </div>
    </div>
    <!-- è¯¦æƒ…弹窗 -->
    <el-dialog v-model="detailDialogVisible" title="库存详情" fullscreen>
      <!-- è¯¦æƒ…内容 -->
    </el-dialog>
  </div>
</template>
```
## 7. Three.js åœºæ™¯è®¾è®¡
### 7.1 åˆå§‹åŒ–流程
1. åˆ›å»º `WebGLRenderer`,挂载到 `canvasContainer`
2. åˆ›å»º `PerspectiveCamera`(透视相机)
3. åˆ›å»º `Scene` åœºæ™¯
4. æ·»åŠ å…‰ç…§ï¼ˆçŽ¯å¢ƒå…‰ + å®šå‘光)
5. åˆ›å»ºåœ°é¢ï¼ˆ`PlaneGeometry` + ç½‘格材质)
6. åˆ›å»ºè´§æž¶è´§ä½ï¼ˆ`InstancedMesh`)
7. æ·»åŠ  `OrbitControls` æŽ§åˆ¶å™¨
8. å¯åŠ¨æ¸²æŸ“å¾ªçŽ¯
### 7.2 è´§ä½å®šä½ç®—法
```
x = (column - maxColumn/2) * CELL_SIZE_X
y = layer * CELL_SIZE_Y
z = (row - maxRow/2) * CELL_SIZE_Z
```
### 7.3 ç‚¹å‡»æ‹¾å–
- ä½¿ç”¨ `Raycaster` è¿›è¡Œå°„线检测
- é€šè¿‡ `instanceId` è¯†åˆ«è¢«ç‚¹å‡»çš„货位实例
- é«˜äº®ï¼šä¸´æ—¶æ›¿æ¢æè´¨é¢œè‰²
### 7.4 ç›¸æœºèšç„¦åŠ¨ç”»
- ä½¿ç”¨ç®€å•线性插值(lerp)平滑移动相机
- ç›®æ ‡ä½ç½®ï¼šè´§ä½åæ ‡ + åç§»é‡
- æ’值公式:`camera.position.lerp(target, 0.05)` æ¯å¸§æ‰§è¡Œ
## 8. æ€§èƒ½ä¼˜åŒ–
| ç­–ç•¥ | è¯´æ˜Ž |
|------|------|
| InstancedMesh | å•次 drawcall æ¸²æŸ“所有货位 |
| è§†é”¥ä½“剔除 | ç›¸æœºå¤–的货位不渲染 |
| é¢œè‰²ç¼“å­˜ | æè´¨å¤ç”¨ï¼Œé¿å…é¢‘繁创建 |
| requestAnimationFrame | æ¸²æŸ“循环使用 RAF |
| æ•°æ®åˆ†é¡µ | å¤§ä»“库可考虑按区域分片加载 |
## 9. è·¯ç”±æ³¨å†Œ
在 `viewGird.js` ä¸­æ³¨å†Œè·¯ç”±ï¼š
```javascript
{
  path: '/stockChat',
  name: 'stockChat',
  component: () => import('@/views/stock/stockChat.vue')
}
```
## 10. å®žçŽ°æ³¨æ„äº‹é¡¹
### 10.1 åŽç«¯äº¤ä»˜ç‰©
- `Get3DLayout` API å®žçŽ°ï¼Œè¿”å›žç»“æž„è§ 4.1
- SignalR Hub é…ç½®ï¼ˆ`/stockHub`),推送 `stockUpdated` äº‹ä»¶
- è´§ä½é¢œè‰²åˆ¤å®šé€»è¾‘按 3.2 è§„则在后端或前端实现均可
### 10.2 å‰ç«¯ç­›é€‰è”动
- `filterStockStatus`、`filterMaterielCode`、`filterBatchNo` ä¸‰è€…联动
- ç­›é€‰ç»“果高亮显示,非匹配货位变暗(opacity: 0.3)
- ç­›é€‰ä¸ºç©ºæ—¶æ˜¾ç¤ºå…¨éƒ¨è´§ä½æ­£å¸¸é¢œè‰²
Code/WMS/docs/superpowers/specs/2026-03-30-ÈÎÎñ¿â´æÀúÊ·¼Ç¼Éè¼Æ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
# ä»»åŠ¡åº“å­˜åŽ†å²è®°å½•è®¾è®¡
## æ¦‚è¿°
在 `TaskService` ä¸­æ³¨å…¥ä»»åŠ¡åŽ†å²å’Œåº“å­˜åŽ†å²æœåŠ¡ï¼Œåœ¨æ‰€æœ‰ä»»åŠ¡å®Œæˆæ–¹æ³•ä¸­ä¿å­˜åŽ†å²è®°å½•ã€‚
## æ¶‰åŠæ–‡ä»¶
- `WIDESEA_TaskInfoService/TaskService.cs`
## è¯¦ç»†è®¾è®¡
### 1. æ³¨å…¥æœåŠ¡
在 `TaskService` ä¸­æ·»åŠ ï¼š
```csharp
private readonly ITask_HtyService _task_HtyService;
private readonly IStockInfo_HtyService _stockInfo_HtyService;
```
构造函数添加参数并赋值。
### 2. ä¿®æ”¹ CompleteTaskAsync
`InboundFinishTaskAsync`、`OutboundFinishTaskAsync`、`RelocationFinishTaskAsync` éƒ½è°ƒç”¨äº† `CompleteTaskAsync`,因此只需修改 `CompleteTaskAsync` å³å¯ã€‚
由于 `Dt_Task` æ²¡æœ‰ `OperateType` å­—段,需要给 `CompleteTaskAsync` æ·»åŠ å‚æ•°ï¼š
**修改方法签名:**
```csharp
private async Task<WebResponseContent> CompleteTaskAsync(Dt_Task task, string operateType)
```
**修改后代码:**
```csharp
var historyTask = _mapper.Map<Dt_Task_Hty>(task);
historyTask.InsertTime = DateTime.Now;
historyTask.OperateType = operateType;
var saveResult = await _task_HtyService.Repository.AddDataAsync(historyTask) > 0;
if (!saveResult) return WebResponseContent.Instance.Error("任务历史保存失败");
return WebResponseContent.Instance.OK("任务完成");
```
**调用方修改(3处):**
- `InboundFinishTaskAsync`:调用 `await CompleteTaskAsync(task, "入库完成")`
- `OutboundFinishTaskAsync`:调用 `await CompleteTaskAsync(task, "出库完成")`
- `RelocationFinishTaskAsync`:调用 `await CompleteTaskAsync(task, "移库完成")`
### 3. InboundFinishTaskTrayAsync å’Œ OutboundFinishTaskTrayAsync
这两个方法**不调用** `CompleteTaskAsync`,需要在事务内、删除任务前添加任务历史和库存历史保存:
```csharp
// ä»»åŠ¡åŽ†å²
var historyTask = _mapper.Map<Dt_Task_Hty>(task);
historyTask.InsertTime = DateTime.Now;
historyTask.OperateType = "空托盘入库完成"; // æˆ–"空托盘出库完成"
if (await _task_HtyService.Repository.AddDataAsync(historyTask) <= 0)
    return content.Error("任务历史保存失败");
// åº“存历史
var historyStock = _mapper.Map<Dt_StockInfo_Hty>(stockInfo);
historyStock.InsertTime = DateTime.Now;
historyStock.OperateType = "空托盘入库完成"; // æˆ–"空托盘出库完成"
if (await _stockInfo_HtyService.Repository.AddDataAsync(historyStock) <= 0)
    return content.Error("库存历史保存失败");
```
### 4. æ“ä½œç±»åž‹æžšä¸¾
| æ–¹æ³• | OperateType |
|------|-------------|
| `InboundFinishTaskAsync` | "入库完成" |
| `OutboundFinishTaskAsync` | "出库完成" |
| `RelocationFinishTaskAsync` | "移库完成" |
| `InboundFinishTaskTrayAsync` | "空托盘入库完成" |
| `OutboundFinishTaskTrayAsync` | "空托盘出库完成" |
## å…³é”®ç‚¹
- `Dt_Task_Hty` ç»§æ‰¿è‡ª `Dt_Task`,包含 `SourceId`(原表主键)、`OperateType`(操作类型)、`InsertTime`(插入时间)
- ä½¿ç”¨ `MapsterMapper` çš„ `_mapper.Map<T>()` è¿›è¡Œå¯¹è±¡æ˜ å°„
- åŽ†å²ä¿å­˜å¿…é¡»åœ¨åˆ é™¤åŽŸè®°å½•**之前**执行