075c5319285ab9896a74655c82cec7c1ae0e0196..b680585c3a6d43f0c72a83a115ea537ce8c91a07
18 小时以前 647556386
Merge branch 'master' of http://115.159.85.185:8098/r/Zho...
b68058 对比 | 目录
18 小时以前 647556386
1
1963e4 对比 | 目录
21 小时以前 pan
提交
912759 对比 | 目录
昨天 huangxiaoqiang
1
f4638b 对比 | 目录
昨天 huangxiaoqiang
1
4619d7 对比 | 目录
昨天 huangxiaoqiang
新增文本日志Web端显示
9ce02d 对比 | 目录
2 天以前 pan
提交
f534cb 对比 | 目录
2 天以前 覃楚胧
数据库字典
2a78fe 对比 | 目录
2 天以前 pan
提交
02aefe 对比 | 目录
已添加3个文件
已修改9个文件
1955 ■■■■■ 文件已修改
项目代码/WIDESEA_WMSClient/src/extension/inbound/extend/EmptyTrayInbound.vue 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/StockSelect.vue 508 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue 487 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/router/viewGird.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/basic/locationInfo.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/inbound/inboundOrder.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/stock/stockView.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/system/Log.vue 346 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/System/Sys_LogController.cs 384 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/数据库字典/ALD_20251212105252.pdf 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/数据库字典/WIDESEAWMS_ALDZhongRui.sql 补丁 | 查看 | 原始文档 | blame | 历史
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/extend/EmptyTrayInbound.vue
@@ -1,17 +1,17 @@
<template>
    <vol-box v-model="show" title="空托入库" :width="800" :height="1200">
        <template #content>
            <el-form ref="form" :model="form" label-width="90px">
                <el-form-item label="入库区域:">
            <el-form ref="form" :model="form" :rules="rules" label-width="90px">
                <el-form-item label="入库区域:" prop="locationType">
                    <el-select v-model="form.locationType" placeholder="请选择入库区域">
                        <el-option v-for="item in locationTypes" :key="item.locationType" :label="item.locationTypeDesc"
                            :value="item.locationType" />
                    </el-select>
                </el-form-item>
                <el-form-item label="托盘条码:">
                    <el-input v-model="form.palletCode" placeholder="请扫描/输入托盘条码" @keyup.enter="submit" @keyup.13="submit"
                        clearable maxlength="50" @paste="handlePaste" @input="handleInput" ref="boxCodeInput" />
                <el-form-item label="托盘条码:" prop="palletCode">
                    <el-input v-model="form.palletCode" placeholder="请扫描/输入托盘条码(A开头,后跟数字)" @keyup.enter="submit" clearable
                        @paste="handlePaste" @input="handleInput" ref="boxCodeInput" />
                </el-form-item>
            </el-form>
        </template>
@@ -34,13 +34,37 @@
        value: { type: Boolean, default: false }
    },
    data() {
        // è‡ªå®šä¹‰æ¡ç éªŒè¯è§„则
        const validatePalletCode = (rule, value, callback) => {
            if (!value) {
                return callback(new Error('请输入托盘条码'));
            }
            // éªŒè¯æ¡ç æ ¼å¼ï¼šA开头,后面至少1位数字(不限制具体长度)
            const codePattern = /^A\d+$/;
            if (!codePattern.test(value)) {
                return callback(new Error('条码格式不正确!正确格式:A开头,后跟数字,如:A000008080'));
            }
            callback();
        };
        return {
            show: false,
            form: {
                palletCode: '',
                locationType: ''
            },
            locationTypes: []
            locationTypes: [],
            // è¡¨å•验证规则
            rules: {
                locationType: [
                    { required: true, message: '请选择入库区域', trigger: 'change' }
                ],
                palletCode: [
                    { validator: validatePalletCode, trigger: ['blur', 'change'] }
                ]
            }
        }
    },
    methods: {
@@ -62,15 +86,13 @@
        },
        async submit() {
            if (!this.form.palletCode) {
                this.$message.warning('请输入托盘条码')
                this.focusInput()
                return
            }
            if (!this.form.locationType) {
                this.$message.warning('请选择入库区域')
                return
            // è¡¨å•验证
            try {
                await this.$refs.form.validate();
            } catch (error) {
                // éªŒè¯å¤±è´¥ï¼Œèšç„¦è¾“入框
                this.focusAndSelectInput();
                return;
            }
            try {
@@ -88,6 +110,8 @@
                    this.$message.success("组盘成功");
                    // æ¸…空输入框数据
                    this.form.palletCode = '';
                    // é‡ç½®éªŒè¯çŠ¶æ€
                    this.$refs.form.clearValidate('palletCode');
                    // èšç„¦å¹¶é€‰ä¸­è¾“入框
                    this.focusAndSelectInput();
                } else {
@@ -104,15 +128,39 @@
        // æ‰«ææžªä¼˜åŒ–处理
        handleInput(value) {
            // è¿‡æ»¤éžæ•°å­—和条码常用字符
            this.form.palletCode = value.replace(/[^a-zA-Z0-9\-]/g, '')
            // è¿‡æ»¤éžæ•°å­—和条码常用字符,允许A开头
            this.form.palletCode = value.replace(/[^a-zA-Z0-9]/g, '')
            // è‡ªåŠ¨è½¬æ¢ä¸ºå¤§å†™ï¼ˆæ¡ç é€šå¸¸ä¸ºå¤§å†™ï¼‰
            this.form.palletCode = this.form.palletCode.toUpperCase();
            // è‡ªåŠ¨è§¦å‘éªŒè¯
            this.$nextTick(() => {
                this.$refs.form.validateField('palletCode');
            });
        },
        handlePaste(e) {
            // ç²˜è´´æ—¶è‡ªåŠ¨æäº¤
            setTimeout(this.submit, 100)
            // èŽ·å–ç²˜è´´çš„å†…å®¹
            const clipboardData = e.clipboardData || window.clipboardData;
            const pastedText = clipboardData.getData('text');
            // å¤„理粘贴内容
            const cleanedText = pastedText.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
            // å¦‚果粘贴内容符合条码格式,自动填充并提交
            if (cleanedText.startsWith('A')) {
                this.form.palletCode = cleanedText;
                // å»¶è¿Ÿæäº¤ï¼Œç¡®ä¿è¡¨å•已更新
                setTimeout(() => {
                    this.submit();
                }, 50);
            }
            // é˜»æ­¢é»˜è®¤ç²˜è´´è¡Œä¸ºï¼Œä½¿ç”¨æˆ‘们处理后的值
            e.preventDefault();
        },
        // èšç„¦å¹¶é€‰ä¸­è¾“入框
        focusAndSelectInput() {
            this.$nextTick(() => {
@@ -129,7 +177,7 @@
                }, 100);
            });
        },
        // åªèšç„¦è¾“入框(不清空数据)
        focusInput() {
            this.$nextTick(() => {
@@ -140,10 +188,14 @@
                }
            });
        },
        // æ¸…空表单数据
        clearForm() {
            this.form.palletCode = '';
            // é‡ç½®éªŒè¯çŠ¶æ€
            if (this.$refs.form) {
                this.$refs.form.clearValidate();
            }
            // ä¸æ¸…空 locationType,保持区域选择
        }
    },
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/extend/StockSelect.vue
@@ -15,11 +15,22 @@
              <el-divider direction="vertical"></el-divider>
              <span class="less-style">物料编号: {{ row.materielCode }} </span>
              <el-divider direction="vertical"></el-divider>
              <span class="less-style">需求数量: {{ row.qty }} </span>
              <span class="less-style"
                >需求数量: {{ row.orderQuantity }}
              </span>
              <el-divider direction="vertical"></el-divider>
              <span :class="selectionClass">已选数量: {{ selectionSum }} </span>
              <span :class="selectionClass"
                >已选数量: {{ selectionSum }}
              </span>
            </el-col>
            <el-col :span="8">
             <!--  <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px"
                @click="getData(false)"
                >{{ kcname }}</el-link
              > -->
              <el-link
                type="primary"
                size="small"
@@ -27,64 +38,17 @@
                @click="getData"
                >刷新</el-link
              >
              <el-link
      <!--         <el-link
                type="primary"
                size="small"
                style="float: right; height: 20px; margin-right: 10px"
                @click="openOutboundDialog"
                >直接出库</el-link
              >
                @click="revokeAssign"
                >撤销分配</el-link
              > -->
            </el-col>
          </el-row>
        </el-alert>
      </div>
      <!-- æ–°å¢žç­›é€‰åŒºåŸŸ -->
      <div class="filter-area" style="margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 4px;">
        <el-form :model="filterForm" inline @submit.prevent>
          <el-form-item label="物料编号:">
            <el-input
              v-model="filterForm.materielCode"
              placeholder="模糊筛选物料编号"
              clearable
              style="width: 180px"
              @input="filterTable"
            ></el-input>
          </el-form-item>
          <el-form-item label="物料条码:">
            <el-input
              v-model="filterForm.barcode"
              placeholder="模糊筛选物料条码"
              clearable
              style="width: 180px"
              @input="filterTable"
            ></el-input>
          </el-form-item>
          <el-form-item label="托盘编号:">
            <el-input
              v-model="filterForm.palletCode"
              placeholder="模糊筛选托盘编号"
              clearable
              style="width: 180px"
              @input="filterTable"
            ></el-input>
          </el-form-item>
          <el-form-item label="货位编号:">
            <el-input
              v-model="filterForm.locationCode"
              placeholder="模糊筛选货位编号"
              clearable
              style="width: 180px"
              @input="filterTable"
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="filterTable">搜索</el-button>
            <el-button @click="resetFilter">重置</el-button>
          </el-form-item>
        </el-form>
      </div>
      <div class="box-table" style="margin-top: 1%">
        <el-table
          ref="singleTable"
@@ -95,6 +59,7 @@
          height="500px"
          @selection-change="handleSelectionChange"
        >
          >
          <el-table-column type="selection" width="55"> </el-table-column>
          <el-table-column
            label="序号"
@@ -127,334 +92,185 @@
        </el-table>
      </div>
      <template #footer>
        <!-- åŽ»æŽ‰é”å®šåº“å­˜æŒ‰é’®ï¼Œåªä¿ç•™å…³é—­æŒ‰é’® -->
       <!--  <el-button type="primary" size="small" @click="outbound"
          >直接出库</el-button
        >
        <el-button type="primary" size="small" @click="lockStock"
          >锁定库存</el-button
        > -->
        <el-button type="danger" size="small" @click="showDetialBox = false"
          >关闭</el-button
        >
      </template>
    </vol-box>
    <!-- å‡ºåº“站台选择弹窗(静态模板实现) -->
    <el-dialog
      v-model="showOutboundDialog"
      title="出库操作 - é€‰æ‹©å‡ºåº“站台"
      width="500px"
      :append-to-body="true"
    >
      <el-form
        :model="outboundForm"
        :rules="outboundRules"
        ref="outboundFormRef"
        label-width="100px"
        style="padding: 0 20px"
      >
        <el-form-item label="出库站台" prop="selectedPlatform" style="margin-bottom: 24px">
          <el-select
            v-model="outboundForm.selectedPlatform"
            placeholder="请选择出库站台(3-12)"
            style="width: 100%; height: 40px"
          >
            <el-option
              v-for="platform in platformOptions"
              :key="platform.value"
              :label="platform.label"
              :value="platform.value"
            ></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showOutboundDialog = false" style="margin-right: 8px">取消</el-button>
        <el-button type="primary" @click="confirmOutbound">确定出库</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script>
import VolBox from "@/components/basic/VolBox.vue";
import { ElMessage } from "element-plus";
import { fa } from "element-plus/es/locales.mjs";
export default {
  components: { VolBox },
  data() {
    return {
      row: null, // æŽ¥æ”¶çˆ¶ç»„件传递的完整数据(包含明细ID集合)
      row: null,
      kcname: "",
      pkcx: false,
      showDetialBox: false,
      tableData: [],
      originalTableData: [], // å­˜å‚¨åŽŸå§‹æ•°æ®ï¼Œç”¨äºŽç­›é€‰
      tableColumns: [
        { prop: "materielCode", title: "物料编号", type: "string", width: 150 },
        { prop: "barcode", title: "物料条码", type: "string", width: 150 },
        { prop: "palletCode", title: "托盘编号", type: "string", width: 150 },
        { prop: "locationCode", title: "货位编号", type: "string", width: 180 },
        { prop: "useableQuantity", title: "可用数量", type: "string" },
        {
          prop: "materielCode",
          title: "物料编号",
          type: "string",
          width: 150,
        },
        {
          prop: "materielName",
          title: "物料名称",
          type: "string",
          width: 150,
        },
        {
          prop: "palletCode",
          title: "托盘编号",
          type: "string",
          width: 150,
        },
        {
          prop: "locationCode",
          title: "货位编号",
          type: "string",
          width: 180,
        },
        {
          prop: "useableQuantity",
          title: "可用数量",
          type: "string",
        },
      ],
      selection: [], // é€‰ä¸­çš„库存数据
      selectionSum: 0, // å·²é€‰æ•°é‡æ€»å’Œ
      selection: [],
      selectionSum: 0,
      selectionClass: "less-style",
      originalQuantity: 0,
      // ç­›é€‰è¡¨å•数据
      filterForm: {
        materielCode: "",
        barcode: "",
        palletCode: "",
        locationCode: ""
      },
      // å‡ºåº“弹窗相关数据
      showOutboundDialog: false,
      outboundForm: { selectedPlatform: "" }, // è¡¨å•绑定数据
      outboundRules: {
        selectedPlatform: [
          { required: true, message: "请选择出库站台", trigger: "change" },
        ],
      },
      platformOptions: [
        { label: "站台2", value: "2-1" },
        { label: "站台3", value: "3-1" },
      ],
      pkcx: false, // æ–°å¢žï¼šé»˜è®¤false(如果需要可从父组件传递)
    };
  },
  methods: {
    // æŽ¥æ”¶çˆ¶ç»„件传递的数据(包含明细ID集合和物料信息)
    open(data) {
      this.row = data; // data结构:{materielCode, materielName, qty, orderNo, detailIds, mainOrderId, groupRow}
    open(row) {
      console.log(row);
      this.row = row;
      this.showDetialBox = true;
      this.getData(); // åŠ è½½åº“å­˜æ•°æ®
      this.updateSelectionClass(); // åˆå§‹åŒ–已选数量样式
      console.log("接收的分组明细ID集合:", this.row.detailIds);
      console.log("查询库存的物料编码:", this.row.materielCode);
    },
    // æ‰“开出库弹窗
    openOutboundDialog() {
      if (this.selection.length === 0) {
        return ElMessage.error("请选择库存数据");
      }
      // æ ¡éªŒæ˜Žç»†ID集合
      if (!this.row?.detailIds || this.row.detailIds.length === 0) {
        return ElMessage.error("没有获取到单据明细ID,无法出库");
      }
      // é‡ç½®è¡¨å•避免残留值
      this.outboundForm.selectedPlatform = "";
      this.showOutboundDialog = true;
    },
    // æ ¸å¿ƒä¿®æ”¹ï¼šURL拼接传递int[] orderDetailId(重复参数名)和station,请求体传库存数据
    confirmOutbound() {
      this.$refs.outboundFormRef.validate((valid) => {
        if (!valid) return;
        // æ ¡éªŒé€‰ä¸­åº“存和明细ID
        if (this.selection.length <= 0) {
          return this.$message.error("请勾选库存数据");
        }
        if (!this.row?.detailIds || this.row.detailIds.length === 0) {
          return this.$message.error("没有获取到单据明细ID,无法出库");
        }
        try {
          // 1. è½¬æ¢ID为整数数组(确保后端能识别为int[])
          const orderDetailId = this.row.detailIds.map(id => {
            const num = Number(id);
            if (isNaN(num) || !Number.isInteger(num)) {
              throw new Error(`ID ${id} ä¸æ˜¯æœ‰æ•ˆçš„æ•´æ•°`);
            }
            return num;
          });
          // 2. æ‹¼æŽ¥URL:int[] ç”¨é‡å¤å‚数名格式(?orderDetailId=1&orderDetailId=2&...)
          let url = "api/Task/GenerateOutboundTask";
          // æ‹¼æŽ¥ID数组参数
          const idParams = orderDetailId.map(id => `orderDetailId=${id}`).join("&");
          // æ‹¼æŽ¥ç«™å°å‚æ•°
          const stationParam = `station=${encodeURIComponent(this.outboundForm.selectedPlatform)}`;
          // å®Œæ•´URL(处理参数拼接逻辑)
          const fullUrl = idParams
            ? `${url}?${idParams}&${stationParam}`
            : `${url}?${stationParam}`;
          console.log("出库请求URL:", fullUrl);
          // 3. å‘送请求:URL拼接ID和站台,请求体传库存数据(适配FromBody)
          this.http
            .post(fullUrl, this.selection, "数据处理中")
            .then((x) => {
              if (!x.status) return this.$message.error(x.message);
              this.$message.success("操作成功");
              this.showDetialBox = false;
              this.$emit("parentCall", ($vue) => {
                $vue.getData(); // åˆ·æ–°çˆ¶ç»„件数据
              });
            })
            .catch((err) => {
              console.error("出库失败:", err);
              this.$message.error(`出库失败:${err.message || '请稍后重试'}`);
            });
        } catch (err) {
          this.$message.error(err.message);
        }
      });
    },
    // æŒ‰ç¬¬ä¸€ä¸ªç‰©æ–™ç¼–码查询库存数据
    getData() {
      const url = "api/StockInfo/GetSelectViewDTOs?materielCode=";
      // ä½¿ç”¨çˆ¶ç»„件传递的物料编码(第一个明细的编码)
      this.http
        .post(
          url + this.row.materielCode + "&orderNo=" + (this.row.upperOrderNo || this.row.orderNo),
          null,
          "查询中"
        )
        .then((x) => {
          this.tableData = x || [];
          this.originalTableData = [...this.tableData]; // ä¿å­˜åŽŸå§‹æ•°æ®
          // åˆ·æ–°åŽæ¸…空之前的选择和计数
          this.clearSelection();
          this.selectionSum = 0;
          this.originalQuantity = 0;
          this.updateSelectionClass();
          // åˆ·æ–°åŽé‡ç½®ç­›é€‰æ¡ä»¶
          this.resetFilter();
        })
        .catch((err) => {
          console.error("库存查询失败:", err);
          ElMessage.error("库存查询失败,请稍后重试");
          this.tableData = [];
          this.originalTableData = [];
        });
    },
    // æ’¤é”€æŒ‡å®šåº“存(如果需要)
    revokeAssign() {
      if (!this.row?.detailIds || this.row.detailIds.length === 0) {
        return ElMessage.error("没有获取到单据明细ID,无法撤销");
      }
      try {
        // ID转为整数数组,拼接URL
        const detailIds = this.row.detailIds.map(id => Number(id));
        const idParams = detailIds.map(id => `detailIds=${id}`).join("&");
        const url = `api/OutboundOrderDetail/RevokeLockOutboundStock?id=${this.row.mainOrderId}&${idParams}`;
        this.http
          .post(url, null, "数据处理中")
          .then((x) => {
            if (!x.status) return ElMessage.error(x.message);
            ElMessage.success("撤销成功");
            this.showDetialBox = false;
            this.$emit("parentCall", ($vue) => {
              $vue.getData();
            });
          });
      } catch (err) {
        this.$message.error(`撤销失败:${err.message}`);
      }
    },
    // å¤„理表格选择变化(计算已选数量)
    handleSelectionChange(val) {
      this.selection = val;
      // è®¡ç®—已选数量(转数字避免字符串拼接)
      this.selectionSum = val.reduce(
        (acc, curr) => acc + Number(curr.useableQuantity || 0),
        0
      ) + this.originalQuantity;
      this.updateSelectionClass();
    },
    // æ›´æ–°å·²é€‰æ•°é‡æ ·å¼ï¼ˆå¯¹æ¯”分组总需求数量)
    updateSelectionClass() {
      if (!this.row) return;
      if (this.selectionSum === this.row.qty) {
      this.originalQuantity = this.row.lockQuantity;
      this.selectionSum = this.row.lockQuantity;
      this.getData();
      if (this.selectionSum == this.row.orderQuantity) {
        this.selectionClass = "equle-style";
      } else if (this.selectionSum < this.row.qty) {
      } else if (this.selectionSum < this.row.orderQuantity) {
        this.selectionClass = "less-style";
      } else {
        this.selectionClass = "more-style";
      }
    },
    // åˆ‡æ¢è¡¨æ ¼é€‰æ‹©
    toggleSelection(rows) {
      rows
        ? rows.forEach((row) => this.$refs.singleTable.toggleRowSelection(row))
        : this.clearSelection();
    lockStock() {
      this.http
        .post(
          "api/OutboundOrderDetail/LockOutboundStock?id=" + this.row.id,
          this.selection,
          "数据处理中"
        )
        .then((x) => {
          if (!x.status) return this.$message.error(x.message);
          this.$message.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
    },
    // æ¸…空选择
    clearSelection() {
      if (this.$refs.singleTable) {
    outbound() {
      if (this.selection.length <= 0) {
        return this.$message.error("请勾选");
      }
      let url = this.pkcx
        ? "api/Task/GenerateOutboundTask?orderDetailId="
        : "api/Task/GenerateOutboundTask?orderDetailId=";
      this.http
        .post(url + this.row.id, this.selection, "数据处理中")
        .then((x) => {
          if (!x.status) return this.$message.error(x.message);
          this.$message.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
    },
    getData(a) {
      if (!a) this.pkcx = !this.pkcx;
      let url = this.pkcx
        ? "api/StockInfo/GetStockSelectViews?materielCode="
        : "api/StockInfo/GetStockSelectViews?materielCode=";
      this.kcname = this.pkcx ? "立库库存" : "平库库存";
      this.http
        .post(
          url + this.row.materielCode + "&orderId=" + this.row.orderId,
          null,
          "查询中"
        )
        .then((x) => {
          this.tableData = x;
        });
    },
    revokeAssign() {
      console.log(this.row);
      this.http
        .post(
          "api/OutboundOrderDetail/RevokeLockOutboundStock?id=" + this.row.id,
          null,
          "数据处理中"
        )
        .then((x) => {
          if (!x.status) return this.$message.error(x.message);
          this.$message.success("操作成功");
          this.showDetialBox = false;
          this.$emit("parentCall", ($vue) => {
            $vue.getData();
          });
        });
    },
    handleSelectionChange(val) {
      this.selection = val;
      this.selectionSum =
        val.reduce(
          (accumulator, currentValue) =>
            accumulator + currentValue["useableQuantity"],
          0
        ) + this.originalQuantity;
      if (this.selectionSum == this.row.orderQuantity) {
        this.selectionClass = "equle-style";
      } else if (this.selectionSum < this.row.orderQuantity) {
        this.selectionClass = "less-style";
      } else {
        this.selectionClass = "more-style";
      }
    },
    toggleSelection(rows) {
      if (rows) {
        rows.forEach((row) => {
          this.$refs.singleTable.toggleRowSelection(row);
        });
      } else {
        this.$refs.singleTable.clearSelection();
      }
    },
    // è¡Œç‚¹å‡»äº‹ä»¶
    clearSelection() {
      this.$refs.singleTable.clearSelection();
    },
    handleRowClick(row) {
      this.$refs.singleTable.toggleRowSelection(row);
    },
    // å›¾æ ‡æŒ‰é’®ç‚¹å‡»å ä½æ–¹æ³•(可根据需求扩展)
    tableButtonClick(row, item) {
      console.log("图标按钮点击:", item.title, row);
    },
    // ç­›é€‰è¡¨æ ¼æ•°æ®
    filterTable() {
      if (!this.originalTableData.length) return;
      // è§£æž„筛选条件并转为小写(忽略大小写)
      const { materielCode, barcode, palletCode, locationCode } = this.filterForm;
      const mc = materielCode.toLowerCase().trim();
      const bc = barcode.toLowerCase().trim();
      const pc = palletCode.toLowerCase().trim();
      const lc = locationCode.toLowerCase().trim();
      // æ¨¡ç³Šç­›é€‰é€»è¾‘
      this.tableData = this.originalTableData.filter(item => {
        // æ¯ä¸ªå­—段都做空值处理和小写转换,支持模糊匹配
        const itemMc = (item.materielCode || "").toLowerCase();
        const itemBc = (item.barcode || "").toLowerCase();
        const itemPc = (item.palletCode || "").toLowerCase();
        const itemLc = (item.locationCode || "").toLowerCase();
        return (
          itemMc.includes(mc) &&
          itemBc.includes(bc) &&
          itemPc.includes(pc) &&
          itemLc.includes(lc)
        );
      });
      // ç­›é€‰åŽæ¸…空选择状态
      this.clearSelection();
      this.selectionSum = 0;
      this.updateSelectionClass();
    },
    // é‡ç½®ç­›é€‰æ¡ä»¶
    resetFilter() {
      this.filterForm = {
        materielCode: "",
        barcode: "",
        palletCode: "",
        locationCode: ""
      };
      // æ¢å¤åŽŸå§‹æ•°æ®
      this.tableData = [...this.originalTableData];
      // é‡ç½®é€‰æ‹©çŠ¶æ€
      this.clearSelection();
      this.selectionSum = 0;
      this.updateSelectionClass();
    }
  },
};
</script>
<style scoped>
.less-style {
  color: black;
@@ -466,12 +282,6 @@
.more-style {
  color: red;
}
/* ç­›é€‰åŒºåŸŸæ ·å¼ä¼˜åŒ– */
.filter-area :deep(.el-form-item) {
  margin-bottom: 0;
  margin-right: 10px;
}
</style>
@@ -486,10 +296,12 @@
.box-table .el-table tbody tr:hover > td {
  background-color: #d8e0d4 !important;
  /* color: #ffffff; */
}
.box-table .el-table tbody tr.current-row > td {
  background-color: #f0f9eb !important;
  /* color: #ffffff; */
}
.el-table .success-row {
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue
@@ -60,7 +60,7 @@
      <div class="box-table" style="margin-top: 1%">
        <el-table
          ref="singleTable"
          :data="groupedTableData"
          :data="tableData"
          style="width: 100%; height: 100%"
          highlight-current-row
          @current-change="handleCurrentChange"
@@ -77,127 +77,38 @@
            align="center"
          ></el-table-column>
          <el-table-column
            prop="materielCode"
            label="物料编号"
            width="120"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="materielName"
            label="物料名称"
            width="150"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="batchNo"
            label="批次号"
            width="90"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="supplyCode"
            label="供应商编号"
            width="90"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="orderQuantity"
            label="单据数量"
            width="90"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="lockQuantity"
            label="锁定数量"
            width="90"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="overOutQuantity"
            label="已出数量"
            width="90"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="moveQty"
            label="挪料数量"
            width="90"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="unit"
            label="单位"
            width="80"
            align="center"
          ></el-table-column>
          <el-table-column
            prop="orderDetailStatus"
            label="订单明细状态"
            width="90"
            v-for="(item, index) in tableColumns.filter((x) => !x.hidden)"
            :key="index"
            :prop="item.prop"
            :label="item.title"
            :width="item.width"
            align="center"
          >
            <template #default="scoped">
              <el-tag size="small">
                {{ getDictionaryForGroup(scoped.row) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column
            prop="assignStock"
            label="指定库存"
            width="90"
            align="center"
            :hidden="mainBusinessType !== '22'"
          >
            <template #default="scoped">
              <el-tooltip
                class="item"
                effect="dark"
                content="指定库存"
                placement="bottom"
              >
                <el-link
                  type="primary"
                  :disabled="getButtonEnable('assignStock', scoped.row)"
                  @click="handleAssignStock(scoped.row)"
              <div v-if="item.type == 'icon'">
                <el-tooltip
                  class="item"
                  effect="dark"
                  :content="item.title"
                  placement="bottom"
                >
                  <i class="el-icon-s-grid" style="font-size: 22px"></i>
                </el-link>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column
            prop="viewDetail"
            label="出库详细"
            width="90"
            align="center"
          >
            <template #default="scoped">
              <el-tooltip
                class="item"
                effect="dark"
                content="查看出库详细"
                placement="bottom"
              >
                <el-link
                  type="primary"
                  @click="handleViewDetail(scoped.row)"
                >
                  <i class="el-icon-s-operation" style="font-size: 22px"></i>
                </el-link>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column
            prop="detailCount"
            label="明细数量"
            width="80"
            align="center"
          >
            <template #default="scoped">
              <el-tag type="info" size="small">
                {{ scoped.row.detailCount }}
              </el-tag>
                  <el-link
                    type="primary"
                    :disabled="getButtonEnable(item.prop, scoped.row)"
                    @click="tableButtonClick(scoped.row, item)"
                  >
                    <i :class="item.icon" style="font-size: 22px"></i>
                  </el-link>
                </el-tooltip>
              </div>
              <div v-else-if="item.type == 'tag'">
                <el-tag size="small">
                  {{ getDictionary(scoped.row, item) }}
                </el-tag>
              </div>
              <div v-else>
                {{ scoped.row[item.prop] }}
              </div>
            </template>
          </el-table-column>
        </el-table>
@@ -218,7 +129,6 @@
import StockSelect from "./StockSelect.vue";
import SelectedStock from "./SelectedStock.vue";
import NoStockOut from "./NoStockOut.vue";
import { stationManager, STATION_STORAGE_KEY } from "@/../src/uitils/stationManager";
import { h, createVNode, render, reactive } from "vue";
import {
  ElDialog,
@@ -241,9 +151,129 @@
      flag: false,
      currentRow: null,
      selection: [],
      tableData: [], // åŽŸå§‹æ•°æ®
      groupedTableData: [], // åˆ†ç»„后的数据
      mainBusinessType: null, // å­˜å‚¨ä¸»å•据的businessType
      tableData: [],
      mainBusinessType: null, // æ–°å¢žï¼šå­˜å‚¨ä¸»å•据的businessType
      tableColumns: [
        {
          prop: "id",
          title: "Id",
          type: "int",
          width: 90,
          hidden: true,
        },
        {
          prop: "orderId",
          title: "出库单主键",
          type: "string",
          width: 90,
          hidden: true,
        },
        {
          prop: "materielCode",
          title: "物料编号",
          type: "string",
          width: 120,
        },
        {
          prop: "materielName",
          title: "物料名称",
          type: "string",
          width: 150,
        },
        {
          prop: "batchNo",
          title: "批次号",
          type: "string",
          width: 90,
        },
        {
          prop: "supplyCode",
          title: "供应商编号",
          type: "string",
          width: 90,
        },
        {
          prop: "orderQuantity",
          title: "单据数量",
          type: "string",
          width: 90,
        },
        {
          prop: "lockQuantity",
          title: "锁定数量",
          type: "int",
          width: 90,
        },
        {
          prop: "overOutQuantity",
          title: "已出数量",
          type: "string",
          width: 90,
        },
        {
          prop: "moveQty",
          title: "挪料数量",
          type: "string",
          width: 90,
        },
        {
          prop: "unit",
          title: "单位",
          type: "string",
          width: 80,
        },
        {
          prop: "orderDetailStatus",
          title: "订单明细状态",
          type: "tag",
          width: 90,
          bindKey: "orderDetailStatusEnum",
        },
        {
          prop: "assignStock",
          title: "指定库存",
          type: "icon",
          width: 90,
          hidden: true, // é»˜è®¤éšè—
          icon: "el-icon-s-grid",
        },
        {
          prop: "viewDetail",
          title: "出库详细",
          type: "icon",
          width: 90,
          icon: "el-icon-s-operation",
        },
        {
          prop: "creater",
          title: "创建人",
          type: "string",
          width: 90,
        },
        {
          prop: "createDate",
          title: "创建时间",
          type: "datetime",
          width: 160,
        },
        {
          prop: "modifier",
          title: "修改人",
          type: "string",
          width: 100,
        },
        {
          prop: "modifyDate",
          title: "修改时间",
          type: "datetime",
          width: 160,
        },
        {
          prop: "remark",
          title: "备注",
          type: "string",
        },
      ],
      paginations: {
        sort: "id",
        order: "desc",
@@ -286,62 +316,16 @@
      dictionaryList: null,
    };
  },
        onMounted() {
      // ä»Žæœ¬åœ°å­˜å‚¨åŠ è½½ä¿å­˜çš„ç«™å°å€¼
      const savedStation = stationManager.getStation();
      console.log(savedStation);
      if (savedStation) {
        outboundForm.selectedPlatform = savedStation;
      } else if (stationOptions.length > 0) {
        // å¦‚果没有保存的值,使用第一个选项
        //stationValue.value = stationOptions[0].value;
  methods: {
    toggleAssignStockColumn() {
      const assignStockColumn = this.tableColumns.find(
        (item) => item.prop === "assignStock"
      );
      if (assignStockColumn) {
        // businessType为22时显示,否则隐藏
        assignStockColumn.hidden = this.mainBusinessType !=='22';
      }
    },
  methods: {
    // æ ¹æ®ç‰©æ–™ç¼–号、批次、供应商分组数据
    groupDataByMaterial() {
      const groups = {};
      this.tableData.forEach(item => {
        // æž„建分组键,批次和供应商可以为空
        const groupKey = `${item.materielCode}_${item.batchNo || ''}_${item.supplyCode || ''}`;
        if (!groups[groupKey]) {
          // åˆ›å»ºæ–°åˆ†ç»„
          groups[groupKey] = {
            materielCode: item.materielCode,
            materielName: item.materielName,
            batchNo: item.batchNo || '-',
            supplyCode: item.supplyCode || '-',
            orderQuantity: 0,
            lockQuantity: 0,
            overOutQuantity: 0,
            moveQty: 0,
            unit: item.unit,
            orderDetailStatus: item.orderDetailStatus,
            detailCount: 0,
            originalDetails: [] // ä¿å­˜åŽŸå§‹æ˜Žç»†æ•°æ®
          };
        }
        // ç´¯åŠ æ•°é‡
        groups[groupKey].orderQuantity += item.orderQuantity || 0;
        groups[groupKey].lockQuantity += item.lockQuantity || 0;
        groups[groupKey].overOutQuantity += item.overOutQuantity || 0;
        groups[groupKey].moveQty += item.moveQty || 0;
        groups[groupKey].detailCount += 1;
        groups[groupKey].originalDetails.push(item);
        // å¦‚果状态不同,可以用第一个状态或特殊处理
        // è¿™é‡Œä½¿ç”¨ç¬¬ä¸€ä¸ªæ˜Žç»†çš„状态
      });
      // è½¬æ¢ä¸ºæ•°ç»„
      this.groupedTableData = Object.values(groups);
      console.log('分组后的数据:', this.groupedTableData);
    },
    open(row) {
      this.row = row;
      this.showDetialBox = true;
@@ -350,8 +334,8 @@
      this.mainBusinessType = row.businessType;
      this.getDictionaryData();
      this.getData();
      this.toggleAssignStockColumn();
    },
    getData() {
      var wheres = [{ name: "orderId", value: this.row.id }];
      var param = {
@@ -365,66 +349,25 @@
        .post("api/OutboundOrderDetail/GetPageData", param, "查询中")
        .then((x) => {
          this.tableData = x.rows;
          // åˆ†ç»„数据
          this.groupDataByMaterial();
          this.toggleAssignStockColumn(); // æ•°æ®åŠ è½½åŽé‡æ–°ç¡®è®¤åˆ—æ˜¾éš
        });
    },
    // é‡ç‚¹ä¿®æ”¹ï¼šå¤„理指定库存点击事件 - ä¼ é€’完整的分组明细信息
    handleAssignStock(row) {
      // ä¼ é€’完整的分组明细数据(包含所有原始明细ID和物料信息)
      if (row.originalDetails && row.originalDetails.length > 0) {
        this.$refs.child.open({
          // ç‰©æ–™åŸºæœ¬ä¿¡æ¯ï¼ˆå–第一个明细的信息)
          materielCode: row.originalDetails[0].materielCode,
          materielName: row.originalDetails[0].materielName,
          // åˆ†ç»„后的总需求数量
          qty: row.orderQuantity,
          // ä¸»å•据编号
          upperOrderNo: this.row.upperOrderNo,
          // å…³é”®ï¼šå½“前分组的所有明细ID集合
          detailIds: row.originalDetails.map(detail => detail.id),
          // ä¸»å•据ID
          mainOrderId: this.row.id,
          // å®Œæ•´åˆ†ç»„行信息(备用)
          groupRow: row
        });
    tableButtonClick(row, column) {
      if (column.prop == "assignStock") {
        this.$refs.child.open(row);
      } else if (column.prop == "NoStockOut") {
        this.$refs.NoStockOut.open(row);
      } else {
        ElMessage.warning("该分组没有明细数据,无法指定库存");
        this.$refs.selectedStock.open(row);
      }
    },
    handleViewDetail(row) {
      // æŸ¥çœ‹åˆ†ç»„明细的详细信息
      if (row.originalDetails && row.originalDetails.length > 0) {
        // è¿™é‡Œå¯ä»¥æ‰“开一个新的弹窗显示所有明细,或者使用第一个明细
        this.$refs.selectedStock.open(row.originalDetails[0]);
      }
    },
    lockstocks() {
      if (this.selection.length === 0) {
        return this.$message.error("请选择单据明细");
      }
      // èŽ·å–æ‰€æœ‰é€‰ä¸­åˆ†ç»„çš„åŽŸå§‹æ˜Žç»†ID
      const detailIds = [];
      this.selection.forEach(group => {
        if (group.originalDetails && group.originalDetails.length > 0) {
          group.originalDetails.forEach(detail => {
            if (detail.id) {
              detailIds.push(detail.id);
            }
          });
        }
      });
      if (detailIds.length === 0) {
        return this.$message.error("没有找到可锁定的明细");
      }
      var keys = this.selection.map((item) => item.id);
      this.http
        .post("api/OutboundOrderDetail/LockOutboundStocks", detailIds, "数据处理中")
        .post("api/OutboundOrderDetail/LockOutboundStocks", keys, "数据处理中")
        .then((x) => {
          if (!x.status) return this.$message.error(x.message);
          this.$message.success("操作成功");
@@ -434,44 +377,22 @@
          });
        });
    },
    handleOpenPicking() {
      this.$router.push({
        path: "/outbound/picking",
        query: { orderId: this.row.id, orderNo: this.row.orderNo },
      });
    },
    handleOpenBatchPicking() {
      this.$router.push({
        path: "/outbound/batchpicking",
        query: { orderId: this.row.id, orderNo: this.row.orderNo },
      });
    },
    outbound() {
        const savedStation = stationManager.getStation();
      console.log(savedStation);
      if (this.selection.length === 0) {
        return this.$message.error("请选择单据明细");
      }
      // èŽ·å–æ‰€æœ‰é€‰ä¸­åˆ†ç»„çš„åŽŸå§‹æ˜Žç»†ID
      const detailIds = [];
      this.selection.forEach(group => {
        if (group.originalDetails && group.originalDetails.length > 0) {
          group.originalDetails.forEach(detail => {
            if (detail.id) {
              detailIds.push(detail.id);
            }
          });
        }
      });
      if (detailIds.length === 0) {
        return this.$message.error("没有找到可出库的明细");
      }
      const platformOptions = [
        { label: "站台2", value: "2-1" },
        { label: "站台3", value: "3-1" },
@@ -483,9 +404,6 @@
        selectedPlatform: platformOptions[0].value,
      });
      if (savedStation) {
        formData.selectedPlatform = savedStation;
      }
      const vnode = createVNode(
        ElDialog,
        {
@@ -530,7 +448,7 @@
                  },
                }, [
                  h(ElSelect, {
                    placeholder: "请选择出库站台",
                    placeholder: "请选择出库站台(3-12)",
                    modelValue: formData.selectedPlatform,
                    "onUpdate:modelValue": (val) => {
                      formData.selectedPlatform = val;
@@ -574,8 +492,9 @@
                        return;
                      }
                      const keys = this.selection.map((item) => item.id);
                      const requestParams = {
                        taskIds: detailIds,
                        taskIds: keys,
                        outboundPlatform: formData.selectedPlatform,
                      };
@@ -612,26 +531,13 @@
      vnode.appContext = this.$.appContext;
      render(vnode, mountNode);
    },
    outboundbatch() {
       const savedStation = stationManager.getStation();
      console.log(savedStation);
      if (this.selection.length === 0) {
        return this.$message.error("请选择单据明细");
      }
      if (this.selection.length > 1) {
        return this.$message.error("只能选择一条单据明细进行分批出库");
      }
      const selectedGroup = this.selection[0];
      if (!selectedGroup.originalDetails || selectedGroup.originalDetails.length === 0) {
        return this.$message.error("没有找到明细数据");
      }
      // åˆ†æ‰¹å‡ºåº“通常针对单个明细
      // è¿™é‡Œä½¿ç”¨ç¬¬ä¸€ä¸ªæ˜Žç»†
      const selectedDetail = selectedGroup.originalDetails[0];
      const platformOptions = [
        { label: "站台2", value: "2-1" },
        { label: "站台3", value: "3-1" },
@@ -643,9 +549,7 @@
        selectedPlatform: platformOptions[0].value,
        outboundDecimal: "",
      });
  if (savedStation) {
        formData.selectedPlatform = savedStation;
      }
      const vnode = createVNode(
        ElDialog,
        {
@@ -704,7 +608,7 @@
                  },
                }, [
                  h(ElSelect, {
                    placeholder: "请选择出库站台",
                    placeholder: "请选择出库站台(3-12)",
                    modelValue: formData.selectedPlatform,
                    "onUpdate:modelValue": (val) => {
                      formData.selectedPlatform = val;
@@ -742,7 +646,6 @@
                    step: "0.01",
                    precision: 2,
                    min: 0.01,
                    max: selectedDetail.orderQuantity - selectedDetail.overOutQuantity - selectedDetail.lockQuantity - selectedDetail.moveQty,
                  }),
                ]),
                h("div", {
@@ -774,8 +677,9 @@
                        return;
                      }
                      const keys = this.selection.map((item) => item.id);
                      const requestParams = {
                        orderDetailId: selectedDetail.id,
                        orderDetailId: keys[0],
                        outboundPlatform: formData.selectedPlatform,
                        batchQuantity: formData.outboundDecimal,
                      };
@@ -810,52 +714,52 @@
        }
      );
      vnode.appContext = this.$appContext;
      vnode.appContext = this.$.appContext;
      render(vnode, mountNode);
    },
    setCurrent(row) {
      this.$refs.singleTable.setCurrentRow(row);
    },
    handleCurrentChange(val) {
      this.currentRow = val;
    },
    getButtonEnable(propName, row) {
      if (propName == "assignStock") {
        if (
          row.orderDetailStatus !== 0 &&
          row.orderDetailStatus !== 70
          row.orderDetailStatus !== 60 &&
          row.orderDetailStatus !== 70 &&
          row.orderDetailStatus !== 80
        ) {
          return false;
        } else {
          return true;
        } else {
          return false;
        }
      }
      return false;
    },
    parentCall(fun) {
      if (typeof fun != "function") {
        return console.log("扩展组件需要传入一个回调方法才能获取父级Vue对象");
      }
      fun(this);
    },
    handleRowClick(row) {
      this.$refs.singleTable.toggleRowSelection(row);
    },
    handleSelectionChange(val) {
      this.selection = val;
    },
    getDictionaryData() {
      if (this.dictionaryList) {
        return;
      }
      var param = ["orderDetailStatusEnum"];
      var param = [];
      this.tableColumns.forEach((x) => {
        if (x.type == "tag" && x.bindKey != "") {
          param.push(x.bindKey);
        }
      });
      this.http
        .post("api/Sys_Dictionary/GetVueDictionary", param, "查询中")
        .then((x) => {
@@ -864,22 +768,21 @@
          }
        });
    },
    getDictionaryForGroup(row) {
    getDictionary(row, column) {
      if (this.dictionaryList) {
        var item = this.dictionaryList.find((x) => x.dicNo == "orderDetailStatusEnum");
        var item = this.dictionaryList.find((x) => x.dicNo == column.bindKey);
        if (item) {
          var dicItem = item.data.find((x) => x.key == row.orderDetailStatus);
          var dicItem = item.data.find((x) => x.key == row[column.prop]);
          if (dicItem) {
            return dicItem.value;
          } else {
            return row.orderDetailStatus;
            return row[column.prop];
          }
        } else {
          return row.orderDetailStatus;
          return row[column.prop];
        }
      }
      return row.orderDetailStatus;
      return row[column.prop];
    },
  },
};
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/router/viewGird.js
@@ -227,7 +227,13 @@
    path:'/printForm',
    name: 'printForm',
    component: () => import('@/views/outbound/printForm.vue') 
  },
  },{
    path: '/Log',
    name: 'Log',
    component: () => import('@/views/system/Log.vue'),
    meta: {
    }
  }
]
export default viewgird   
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/basic/locationInfo.vue
@@ -48,7 +48,7 @@
      [
        { title: "货位编号", field: "locationCode", type: "like" },
        { title: "巷道编号", field: "roadwayNo",type:"like" },
        { title: "货位类型", field: "locationType",type: "select",dataKey: "locationTypeEnum",data: [], },
        { title: "货位区域", field: "locationType",type: "select",dataKey: "locationTypeEnum",data: [], },
        { title: "禁用状态", field: "enableStatus" ,type: "select",dataKey: "enableStatusEnum",data: [],},
      ],
      [
@@ -132,7 +132,7 @@
      },
      {
        field: "locationType",
        title: "货位类型",
        title: "货位区域",
        type: "string",
        width: 120,
        align: "left",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/inbound/inboundOrder.vue
@@ -87,7 +87,7 @@
    });
    const searchFormOptions = ref([
      [
        { title: "扫描单据编号或条码", field: "inboundOrderNo", type: "like" },
        { title: "扫描单据或条码", field: "inboundOrderNo", type: "like" },
        { title: "上游单据编号", field: "upperOrderNo", type: "like" },
        {
          title: "业务类型",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/stock/stockView.vue
@@ -158,7 +158,7 @@
      },
      {
        field: "locationType",
        title: "货位类型",
        title: "货位区域",
        type: "string",
        width: 140,
        align: "left",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/system/Log.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,346 @@
<template>
  <div class="tree-container">
    <el-input placeholder="输入关键字进行过滤" v-model="filterText" class="filter-input">
    </el-input>
    <div class="custom-tree-wrapper">
      <el-tree ref="tree" class="filter-tree" :filter-node-method="filterNode" :data="data" node-key="id" accordion>
        <template #default="{ node, data }">
          <div class="custom-tree-node">
            <span class="node-label">{{ node.label }}</span>
            <span v-if="data.hidden" class="node-actions">
              <el-button type="text" size="mini" @click="() => view(data)" class="action-btn">
                æŸ¥çœ‹
              </el-button>
              <el-button type="text" size="mini" @click="() => dowmload(node, data)" class="action-btn">
                ä¸‹è½½
              </el-button>
            </span>
          </div>
        </template>
      </el-tree>
    </div>
  </div>
  <div class="log-container">
    <el-card shadow="always" v-if="logName" class="log-card">
      <template #header>
        <div class="card-header">
          <el-tag type="info" size="small">日志文件</el-tag>
          <span class="log-title">{{ logName }}</span>
        </div>
      </template>
      <div class="log-content">
        <div v-for="(item, index) in log" :key="index" class="log-line">
          <span class="line-number">{{ index + 1 }}</span>
          <span class="line-content">{{ item }}</span>
        </div>
      </div>
    </el-card>
    <div v-else class="empty-log">
      <el-empty description="请选择日志文件进行查看" />
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      data: [],
      defaultProps: {
        children: "children",
        label: "label",
      },
      filterText: "",
      logName: "",
      log: [],
    };
  },
  watch: {
    filterText(val) {
      this.$refs.tree.filter(val);
    },
  },
  created() {
    this.getLogName();
  },
  methods: {
    filterNode(value, data) {
      if (!value) return true;
      return data.label.indexOf(value) !== -1;
    },
    getLogName() {
      this.http
        .post("/api/Sys_Log/GetLogName", null, "正在执行中...")
        .then((x) => {
          if (x.status) {
            this.data = x.data;
          }
        });
    },
    view(data) {
      // var params = {
      //   MainData: { fileName: data.label },
      // };
      this.http
        .post("/api/Sys_Log/GetLog?fileName=" + data.label, "正在查询中...")
        .then((x) => {
          if (x.status) {
            this.logName = data.label;
            this.log = x.data;
          }
        });
    },
    dowmload(node, data) {
      let ipAddress = this.http.ipAddress;
      let url =
        "api/Sys_Log/DownLoadLog?fileName=" +
        data.fatherNode +
        "\\" +
        data.label;
      let fileName = data.label;
      let xmlResquest = new XMLHttpRequest();
      xmlResquest.open("GET", ipAddress + url, true);
      xmlResquest.setRequestHeader("Content-type", "application/json");
      xmlResquest.setRequestHeader(
        "Authorization",
        this.$store.getters.getToken()
      );
      let elink = this.$refs.template;
      xmlResquest.responseType = "blob";
      let $_vue = this;
      this.loadingStatus = true;
      xmlResquest.onload = function (e) {
        // è¯·æ±‚成功
        if (this.status == 200) {
          let blob = this.response;
          let a = document.createElement("a");
          //window.URL.createObjectURL() é™æ€æ–¹æ³•会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL çš„生命周期和创建它的窗口中的 document ç»‘定。这个新的URL å¯¹è±¡è¡¨ç¤ºæŒ‡å®šçš„ File å¯¹è±¡æˆ– Blob å¯¹è±¡ã€‚
          let url = window.URL.createObjectURL(blob);
          a.href = url;
          a.download = fileName;
          a.click();
          //URL.revokeObjectURL() é™æ€æ–¹æ³•用来释放一个之前已经存在的、通过调用 URL.createObjectURL() åˆ›å»ºçš„ URL å¯¹è±¡ã€‚当你结束使用某个 URL å¯¹è±¡ä¹‹åŽï¼Œåº”该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了。
          window.URL.revokeObjectURL(url);
        } else {
          //下载失败处理
        }
      };
      xmlResquest.send();
    },
  },
};
</script>
<style scoped>
.tree-container {
  width: 30%;
  float: left;
  padding: 16px;
  background: #f8f9fa;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.filter-input {
  margin-bottom: 16px;
}
.filter-input :deep(.el-input__inner) {
  border-radius: 20px;
  border-color: #dcdfe6;
  transition: all 0.3s;
}
.filter-input :deep(.el-input__inner):focus {
  border-color: #409eff;
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.custom-tree-wrapper {
  height: 760px;
  overflow-y: auto;
  padding: 8px;
  background: white;
  border-radius: 6px;
  border: 1px solid #ebeef5;
}
.filter-tree :deep(.el-tree-node__content) {
  height: 40px;
  margin: 2px 0;
  border-radius: 4px;
  transition: all 0.2s;
}
.filter-tree :deep(.el-tree-node__content:hover) {
  background-color: #f0f9ff;
}
.filter-tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
  background-color: #ecf5ff;
  font-weight: 600;
}
.custom-tree-node {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0 8px;
}
.node-label {
  font-size: 14px;
  color: #606266;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.node-actions {
  display: flex;
  gap: 8px;
  margin-left: 12px;
}
.action-btn {
  padding: 4px 8px;
  font-size: 12px;
  color: #409eff;
  border-radius: 3px;
}
.action-btn:hover {
  background-color: rgba(64, 158, 255, 0.1);
}
.log-container {
  width: 68%;
  float: right;
  padding: 16px;
}
.log-card {
  height: 800px;
  border-radius: 8px;
  border: 1px solid #ebeef5;
}
.log-card :deep(.el-card__header) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-bottom: 1px solid #ebeef5;
  padding: 16px 20px;
}
.card-header {
  display: flex;
  align-items: center;
  gap: 12px;
}
.log-title {
  font-size: 16px;
  font-weight: 600;
  color: white;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.log-content {
  height: 700px;
  overflow-y: auto;
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
  background: #f8f9fa;
  padding: 12px;
  border-radius: 4px;
}
.log-line {
  display: flex;
  align-items: flex-start;
  margin-bottom: 4px;
  line-height: 1.5;
  background: white;
  padding: 8px 12px;
  border-radius: 4px;
  border-left: 3px solid #409eff;
  transition: all 0.2s;
}
.log-line:hover {
  background: #f0f9ff;
  transform: translateX(2px);
}
.line-number {
  display: inline-block;
  min-width: 40px;
  padding-right: 12px;
  text-align: right;
  color: #909399;
  font-size: 12px;
  user-select: none;
}
.line-content {
  flex: 1;
  color: #303133;
  font-size: 13px;
  word-break: break-all;
  white-space: pre-wrap;
}
.empty-log {
  height: 800px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-log :deep(.el-empty__description) {
  margin-top: 8px;
}
/* æ»šåŠ¨æ¡æ ·å¼ */
.custom-tree-wrapper::-webkit-scrollbar,
.log-content::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
.custom-tree-wrapper::-webkit-scrollbar-track,
.log-content::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 3px;
}
.custom-tree-wrapper::-webkit-scrollbar-thumb,
.log-content::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 3px;
}
.custom-tree-wrapper::-webkit-scrollbar-thumb:hover,
.log-content::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}
/* æ¸…除浮动 */
.tree-container::after,
.log-container::after {
  content: "";
  display: table;
  clear: both;
}
</style>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -714,7 +714,7 @@
                    _logger.LogInformation($"InPickTaskCompleted AddLocationStatusChangeRecord : {ex.Message} ");
                }
                _logger.LogInformation($"托盘回库完成处理成功 - ä»»åŠ¡å·: {task.TaskNum}, æ‰˜ç›˜: {task.PalletCode}, è®¢å•: {task.OrderNo}");
                _logger.LogInformation($"托盘回库完成处理成功 - ä»»åŠ¡å·: {task.TaskNum}, æ‰˜ç›˜: {task.PalletCode}, è®¢å•: {task.OrderNo} è´§ä½çŠ¶æ€ï¼š{locationInfo.LocationStatus}");
                _ = Task.Run(async () =>
                {
                    try
@@ -1136,33 +1136,36 @@
                                allocatefeedmodel.Details.Add(detailModel);
                            }
                            var groupedResult = allocatefeedmodel.Details.GroupBy(item => new
                            {
                                item.WarehouseCode,
                                item.MaterialCode,
                                item.Unit,
                                item.LineNo
                            }).Select(group => new AllocateDtoDetail
                            {
                                 WarehouseCode = group.Key.WarehouseCode,
                                 MaterialCode = group.Key.MaterialCode,
                                 LineNo = group.Key.LineNo,
                                 Qty = group.Sum(x => x.Qty),
                                 Unit = group.Key.Unit,
                                 Barcodes = group.SelectMany(x => x.Barcodes)
                                                       .GroupBy(b => b.Barcode)
                                                       .Select(b => new BarcodeInfo
                                                       {
                                                           Barcode = b.Key,
                                                           BatchNo = b.First().BatchNo,
                                                           SupplyCode = b.First().SupplyCode,
                                                           Qty = b.Max(x => x.Qty),
                                                           Unit = b.First().Unit
                                                       }) .ToList()
                             }) .ToList();
                            var groupedResult = allocatefeedmodel.Details
                                .GroupBy(item => new { item.WarehouseCode, item.MaterialCode, item.Unit, item.LineNo })
                                .Select(group =>
                                {
                                    var deduplicatedBarcodes = group.SelectMany(x => x.Barcodes)
                                                                   .GroupBy(b => b.Barcode)
                                                                   .Select(b => new BarcodeInfo
                                                                   {
                                                                       Barcode = b.Key,
                                                                       BatchNo = b.First().BatchNo,
                                                                       SupplyCode = b.First().SupplyCode,
                                                                       Qty = b.Max(x => x.Qty),
                                                                       Unit = b.First().Unit
                                                                   }).ToList();
                                    return new AllocateDtoDetail
                                    {
                                        WarehouseCode = group.Key.WarehouseCode,
                                        MaterialCode = group.Key.MaterialCode,
                                        LineNo = group.Key.LineNo,
                                        Qty = deduplicatedBarcodes.Sum(b => b.Qty),
                                        Unit = group.Key.Unit,
                                        Barcodes = deduplicatedBarcodes
                                    };
                                }).ToList();
                            allocatefeedmodel.Details = groupedResult;
                          var result = await _invokeMESService.FeedbackAllocate(allocatefeedmodel);
                            var result = await _invokeMESService.FeedbackAllocate(allocatefeedmodel);
                            if (result != null && result.code == 200)
                            {
                                await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
@@ -1175,6 +1178,19 @@
                                          ReturnToMESStatus = 1,
                                      }).Where(x => x.OrderNo == orderNo).ExecuteCommandAsync();
                            }
                            else
                            {
                                await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                                 .SetColumns(x => x.ReturnToMESStatus == 2)
                                 .Where(x => x.OrderId == outboundOrder.Id)
                                 .ExecuteCommandAsync();
                                await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                                    .SetColumns(it => new Dt_OutboundOrder { ReturnToMESStatus = 2, Remark = result.message })
                                     .Where(x => x.OrderNo == orderNo)
                                    .ExecuteCommandAsync();
                            }
                        }
                    }
                    else if (outboundOrder.OrderType == OutOrderTypeEnum.ReCheck.ObjToInt())
@@ -1247,30 +1263,32 @@
                                }
                                feedmodel.details.Add(detailModel);
                            }
                            var groupedResult = feedmodel.details.GroupBy(item => new
                            {
                                item.warehouseCode,
                                item.materialCode,
                                item.unit,
                                item.lineNo
                            }).Select(group => new FeedbackOutboundDetailsModel
                            {
                                warehouseCode = group.Key.warehouseCode,
                                materialCode = group.Key.materialCode,
                                lineNo = group.Key.lineNo,
                                qty = group.Sum(x => x.qty),
                                unit = group.Key.unit,
                                barcodes = group.SelectMany(x => x.barcodes)
                                                       .GroupBy(b => b.barcode)
                                                       .Select(b => new WIDESEA_DTO.Outbound.BarcodesModel
                                                       {
                                                           barcode = b.Key,
                                                           batchNo = b.First().batchNo,
                                                           supplyCode = b.First().supplyCode,
                                                           qty = b.Max(x => x.qty),
                                                           unit = b.First().unit
                                                       }).ToList()
                            }).ToList();
                            var groupedResult = feedmodel.details
                               .GroupBy(item => new { item.warehouseCode, item.materialCode, item.unit, item.lineNo })
                               .Select(group =>
                               {
                                   var deduplicatedBarcodes = group.SelectMany(x => x.barcodes)
                                                                  .GroupBy(b => b.barcode)
                                                                  .Select(b => new WIDESEA_DTO.Outbound.BarcodesModel
                                                                  {
                                                                      barcode = b.Key,
                                                                      batchNo = b.First().batchNo,
                                                                      supplyCode = b.First().supplyCode,
                                                                      qty = b.Max(x => x.qty),
                                                                      unit = b.First().unit
                                                                  }).ToList();
                                   return new FeedbackOutboundDetailsModel
                                   {
                                       warehouseCode = group.Key.warehouseCode,
                                       materialCode = group.Key.materialCode,
                                       lineNo = group.Key.lineNo,
                                       qty = deduplicatedBarcodes.Sum(b => b.qty),
                                       unit = group.Key.unit,
                                       barcodes = deduplicatedBarcodes
                                   };
                               }).ToList();
                            feedmodel.details = groupedResult;
                            var result = await _invokeMESService.FeedbackOutbound(feedmodel);
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/System/Sys_LogController.cs
@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Text;
using WIDESEA_Core;
using WIDESEA_Core.BaseController;
using WIDESEA_ISystemService;
using WIDESEA_Model.Models;
@@ -13,8 +16,385 @@
    [ApiController]
    public class Sys_LogController : ApiBaseController<ISys_LogService, Sys_Log>
    {
        // é…ç½®å¸¸é‡
        private const int MAX_FILE_SIZE_MB = 50;
        private const int MAX_RETRY_COUNT = 3;
        private const int RETRY_DELAY_MS = 100;
        private static readonly string[] ALLOWED_FILE_TYPES = { ".txt", ".log", ".csv", ".json", ".xml" };
        public Sys_LogController(ISys_LogService service) : base(service)
        {
        }
        [HttpPost, Route("GetLogName"), AllowAnonymous]
        public WebResponseContent GetLogName()
        {
            WebResponseContent content = new WebResponseContent();
            try
            {
                List<object> data = new List<object>();
                DirectoryInfo folder = new DirectoryInfo(AppContext.BaseDirectory + "\\logs\\");
                DirectoryInfo[] firstDirectoryInfos = folder.GetDirectories().OrderByDescending(x => x.CreationTime).ToArray();
                int k = 2020;
                for (int i = 0; i < firstDirectoryInfos.Length; i++)
                {
                    if (firstDirectoryInfos[i].Name != "Info")
                    {
                        FileInfo[] nextFileInfos = firstDirectoryInfos[i].GetFiles();
                        List<object> values = new List<object>();
                        for (int j = 0; j < nextFileInfos.Length; j++)
                        {
                            values.Add(new { label = nextFileInfos[j].Name, id = k, hidden = true, fatherNode = firstDirectoryInfos[i].Name });
                            k++;
                        }
                        data.Add(new { label = firstDirectoryInfos[i].Name, children = values, id = i, hidden = false });
                    }
                }
                FileInfo[] nextFileInfo = folder.GetFiles();
                List<object> value = new List<object>();
                for (int j = 0; j < nextFileInfo.Length; j++)
                {
                    value.Add(new { label = nextFileInfo[j].Name, id = k, hidden = true, fatherNode = folder.Name });
                    k++;
                }
                data.Add(new { label = folder.Name, children = value, id = 1, hidden = false });
                return WebResponseContent.Instance.OK(data: data);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error(ex.Message);
            }
        }
        [HttpPost, Route("GetLog"), AllowAnonymous]
        public WebResponseContent GetLog(string fileName)
        {
            WebResponseContent content = new WebResponseContent();
            try
            {
                List<FileInfo> files = new List<FileInfo>();
                DirectoryInfo folder = new DirectoryInfo(AppContext.BaseDirectory + "\\logs\\");
                DirectoryInfo[] firstDirectoryInfos = folder.GetDirectories();
                for (int i = 0; i < firstDirectoryInfos.Length; i++)
                {
                    FileInfo[] nextFileInfos = firstDirectoryInfos[i].GetFiles();
                    files.AddRange(nextFileInfos);
                }
                FileInfo[] nextFileInfo = folder.GetFiles();
                files.AddRange(nextFileInfo);
                if (files.Count > 0)
                {
                    FileInfo file = files.Where(x => x.Name == fileName).FirstOrDefault();
                    if (file == null)
                    {
                        return WebResponseContent.Instance.Error($"未找到日志文件: {fileName}");
                    }
                    // ä½¿ç”¨å…±äº«è¯»å–模式
                    using (FileStream stream = new FileStream(
                        file.FullName,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.ReadWrite))
                    using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
                    {
                        StringBuilder text = new StringBuilder();
                        List<string> lines = new List<string>();
                        while (!reader.EndOfStream)
                        {
                            var line = reader.ReadLine();
                            lines.Add(line);
                        }
                        content = WebResponseContent.Instance.OK(data: lines);
                    }
                }
                else
                {
                    content = WebResponseContent.Instance.Error($"未找到日志文件,【{fileName}】");
                }
            }
            catch (IOException ex)
            {
                if (IsFileLockedException(ex))
                {
                    content = WebResponseContent.Instance.Error($"日志文件正在被系统写入,请稍后再试");
                }
                else
                {
                    content = WebResponseContent.Instance.Error($"打开日志文件错误,{ex.Message}");
                }
            }
            catch (Exception ex)
            {
                content = WebResponseContent.Instance.Error($"打开日志文件错误,{ex.Message}");
            }
            return content;
        }
        [HttpPost, HttpGet, Route("DownLoadLog"), AllowAnonymous]
        public virtual async Task<ActionResult> DownLoadLog(string fileName)
        {
            try
            {
                // 1. å‚数验证
                if (string.IsNullOrWhiteSpace(fileName))
                {
                    return BadRequest("文件名不能为空");
                }
                // å®‰å…¨æ€§æ£€æŸ¥ï¼šé˜²æ­¢è·¯å¾„遍历攻击
                if (fileName.Contains("..") || Path.IsPathRooted(fileName))
                {
                    return BadRequest("无效的文件名");
                }
                //string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
                string logDirectory = Path.Combine(AppContext.BaseDirectory);
                if (!Directory.Exists(logDirectory))
                {
                    Directory.CreateDirectory(logDirectory);
                }
                string filePath = Path.Combine(logDirectory, fileName);
                if (Directory.Exists(filePath))
                {
                    return NotFound($"文件 {fileName} ä¸å­˜åœ¨");
                }
                string extension = Path.GetExtension(fileName).ToLowerInvariant();
                if (!IsAllowedFileType(extension))
                {
                    return BadRequest($"不支持的文件类型: {extension}");
                }
                FileInfo fileInfo = new FileInfo(filePath);
                if (fileInfo.Length > MAX_FILE_SIZE_MB * 1024 * 1024)
                {
                    return BadRequest($"文件过大,超过{MAX_FILE_SIZE_MB}MB限制");
                }
                // æ–¹æ¡ˆ1:使用重试机制 + å…±äº«è¯»å–(推荐)
                byte[] fileBytes = await ReadFileWithRetryAsync(filePath);
                if (fileBytes == null)
                {
                    return StatusCode(500, "文件被占用,无法下载,请稍后重试");
                }
                string contentType = GetContentType(extension);
                // è®¾ç½®ä¸‹è½½å¤´
                Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{System.Web.HttpUtility.UrlEncode(fileName, Encoding.UTF8)}\"");
                Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
                Response.Headers.Add("Pragma", "no-cache");
                Response.Headers.Add("Expires", "0");
                return File(fileBytes, contentType, fileName);
            }
            catch (UnauthorizedAccessException)
            {
                return StatusCode(403, "没有访问该文件的权限");
            }
            catch (PathTooLongException)
            {
                return BadRequest("文件路径过长");
            }
            catch (IOException ex)
            {
                if (IsFileLockedException(ex))
                {
                    return StatusCode(500, "文件被锁定,可能正在被系统写入,请稍后再试");
                }
                return StatusCode(500, $"文件读取失败: {ex.Message}");
            }
            catch (Exception ex)
            {
                // è®°å½•异常日志(这里简化为返回,实际项目中应该记录到日志系统)
                return StatusCode(500, $"服务器内部错误: {ex.Message}");
            }
        }
        /// <summary>
        /// å¸¦é‡è¯•机制的文件读取方法
        /// </summary>
        private async Task<byte[]> ReadFileWithRetryAsync(string filePath)
        {
            for (int attempt = 0; attempt < MAX_RETRY_COUNT; attempt++)
            {
                try
                {
                    // ä½¿ç”¨ FileShare.ReadWrite å…è®¸å…¶ä»–进程同时读取和写入
                    // ä½¿ç”¨å¼‚步读取提高性能
                    using (var fileStream = new FileStream(
                        filePath,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.ReadWrite | FileShare.Delete, // å…è®¸åˆ é™¤æ“ä½œ
                        bufferSize: 4096,
                        useAsync: true))
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            await fileStream.CopyToAsync(memoryStream);
                            return memoryStream.ToArray();
                        }
                    }
                }
                catch (IOException) when (attempt < MAX_RETRY_COUNT - 1)
                {
                    // å¦‚果不是最后一次重试,等待一段时间
                    await Task.Delay(RETRY_DELAY_MS * (attempt + 1));
                }
                catch (IOException ex)
                {
                    // æœ€åŽä¸€æ¬¡å°è¯•也失败了
                    throw;
                }
            }
            return null;
        }
        /// <summary>
        /// åˆ¤æ–­æ˜¯å¦ä¸ºæ–‡ä»¶è¢«é”å®šçš„异常
        /// </summary>
        private bool IsFileLockedException(IOException ex)
        {
            int errorCode = ex.HResult & 0xFFFF;
            return errorCode == 32 || errorCode == 33; // ERROR_SHARING_VIOLATION or ERROR_LOCK_VIOLATION
        }
        /// <summary>
        /// æ£€æŸ¥æ–‡ä»¶ç±»åž‹æ˜¯å¦å…è®¸
        /// </summary>
        private bool IsAllowedFileType(string extension)
        {
            return ALLOWED_FILE_TYPES.Contains(extension);
        }
        /// <summary>
        /// èŽ·å–Content-Type
        /// </summary>
        private string GetContentType(string extension)
        {
            return extension.ToLowerInvariant() switch
            {
                ".txt" => "text/plain; charset=utf-8",
                ".log" => "text/plain; charset=utf-8",
                ".csv" => "text/csv; charset=utf-8",
                ".json" => "application/json; charset=utf-8",
                ".xml" => "application/xml; charset=utf-8",
                _ => "application/octet-stream"
            };
        }
        /// <summary>
        /// å¤‡é€‰æ–¹æ¡ˆï¼šåˆ›å»ºä¸´æ—¶å‰¯æœ¬ä¸‹è½½ï¼ˆæœ€å®‰å…¨ï¼Œä½†æ€§èƒ½ç¨å·®ï¼‰
        /// </summary>
        [HttpPost, HttpGet, Route("DownLoadLogCopy"), AllowAnonymous]
        public virtual async Task<ActionResult> DownLoadLogCopy(string fileName)
        {
            try
            {
                // å‚数验证(同上)
                if (string.IsNullOrWhiteSpace(fileName))
                {
                    return BadRequest("文件名不能为空");
                }
                string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
                string filePath = Path.Combine(logDirectory, fileName);
                if (Directory.Exists(filePath))
                {
                    return NotFound($"文件 {fileName} ä¸å­˜åœ¨");
                }
                // ç”Ÿæˆä¸´æ—¶æ–‡ä»¶å
                string tempFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_{Guid.NewGuid():N}{Path.GetExtension(fileName)}";
                string tempFilePath = Path.Combine(Path.GetTempPath(), tempFileName);
                try
                {
                    // å°è¯•复制文件到临时位置(使用重试机制)
                    bool copySuccess = false;
                    for (int attempt = 0; attempt < MAX_RETRY_COUNT; attempt++)
                    {
                        try
                        {
                            //Directory.GetFiles.Copy(filePath, tempFilePath, false);
                            copySuccess = true;
                            break;
                        }
                        catch (IOException) when (attempt < MAX_RETRY_COUNT - 1)
                        {
                            await Task.Delay(RETRY_DELAY_MS * (attempt + 1));
                        }
                    }
                    if (!copySuccess)
                    {
                        return StatusCode(500, "无法复制文件,可能被其他进程占用");
                    }
                    // ä»Žä¸´æ—¶æ–‡ä»¶è¯»å–
                    byte[] fileBytes;
                    using (FileStream tempStream = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        using (MemoryStream memoryStream = new MemoryStream())
                        {
                            await tempStream.CopyToAsync(memoryStream);
                            fileBytes = memoryStream.ToArray();
                        }
                    }
                    string extension = Path.GetExtension(fileName).ToLowerInvariant();
                    string contentType = GetContentType(extension);
                    // è¿”回文件后清理临时文件
                    var result = File(fileBytes, contentType, fileName);
                    // å¼‚步清理临时文件
                    _ = Task.Run(() =>
                    {
                        try
                        {
                            Directory.Delete(tempFilePath);
                        }
                        catch
                        {
                            // å¿½ç•¥åˆ é™¤å¤±è´¥
                        }
                    });
                    return result;
                }
                finally
                {
                    // ç¡®ä¿ä¸´æ—¶æ–‡ä»¶è¢«æ¸…理
                    if (Directory.Exists(tempFilePath))
                    {
                        try
                        {
                            Directory.Delete(tempFilePath);
                        }
                        catch
                        {
                            // å¿½ç•¥åˆ é™¤å¤±è´¥
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"服务器内部错误: {ex.Message}");
            }
        }
    }
}
}
ÏîÄ¿×ÊÁÏ/Êý¾Ý¿â×Öµä/ALD_20251212105252.pdf
Binary files differ
ÏîÄ¿×ÊÁÏ/Êý¾Ý¿â×Öµä/WIDESEAWMS_ALDZhongRui.sql
Binary files differ