yuan
9 天以前 d78815179b7d49509e6637d547ca8bbd525f7fcf
Merge remote-tracking branch 'origin/dev_pro_河南鹤壁' into dev_pro_河南鹤壁

# Conflicts:
# multiple/config.json
已添加17个文件
已修改26个文件
2999 ■■■■■ 文件已修改
multiple/assets/favicon/CHMYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HMCLfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JMTMfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/SDKSfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/SJDQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/TXDQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/CHMYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HMCLLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JMTMLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/SDKSLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/SJDQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/TXDQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProcess.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/nonconformingManagement.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/filePreview/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/ProductSelectDialog.vue 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/viewDetailDia.vue 630 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairDetailModal.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/New.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/logininfor/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/operlog/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentLedger/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementPlan/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 277 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 405 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/PurchaseLedgerAddDialog.vue 856 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/ProcessSelectDialog.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionTraceability/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/components/detailModal.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/CHMYfavicon.ico
multiple/assets/favicon/HMCLfavicon.ico
multiple/assets/favicon/JMTMfavicon.ico
multiple/assets/favicon/SDKSfavicon.ico
multiple/assets/favicon/SJDQfavicon.ico
multiple/assets/favicon/TXDQfavicon.ico
multiple/assets/logo/CHMYLogo.png
multiple/assets/logo/HMCLLogo.png
multiple/assets/logo/JMTMLogo.png
multiple/assets/logo/SDKSLogo.png
multiple/assets/logo/SJDQLogo.png
multiple/assets/logo/TXDQLogo.png
multiple/config.json
@@ -153,8 +153,17 @@
  "DDGW": {
    "env": {
      "VITE_APP_TITLE": "河南东大高温节能材料有限公司",
      "VITE_BASE_API": "http://103.189.141.42:9001",
      "VITE_JAVA_API": "http://103.189.141.42:9002"
      "VITE_BASE_API": "http://36.133.40.190:81",
      "VITE_JAVA_API": "http://36.133.40.190:82"
    },
    "logo": "logo/DDGWLogo.png",
    "favicon": "favicon/DDGWfavicon.ico"
  },
  "DDGW-TEST": {
    "env": {
      "VITE_APP_TITLE": "河南东大高温节能材料有限公司",
      "VITE_BASE_API": "http://36.133.40.190:83",
      "VITE_JAVA_API": "http://36.133.40.190:84"
    },
    "logo": "logo/DDGWLogo.png",
    "favicon": "favicon/DDGWfavicon.ico"
@@ -167,6 +176,60 @@
    },
    "logo": "logo/XHZGLogo.png",
    "favicon": "favicon/XHZGfavicon.ico"
  },
  "SDKS": {
    "env": {
      "VITE_APP_TITLE": "鹤壁市四达矿山设备有限公司",
      "VITE_BASE_API": "http://36.213.180.217:9520",
      "VITE_JAVA_API": "http://36.213.180.217:9521"
    },
    "logo": "logo/SDKSLogo.png",
    "favicon": "favicon/SDKSfavicon.ico"
  },
  "JMTM": {
    "env": {
      "VITE_APP_TITLE": "河南捷美特镁科技有限公司",
      "VITE_BASE_API": "http://36.133.40.61:81",
      "VITE_JAVA_API": "http://36.133.40.61:82"
    },
    "logo": "logo/JMTMLogo.png",
    "favicon": "favicon/JMTMfavicon.ico"
  },
  "CHMY": {
    "env": {
      "VITE_APP_TITLE": "鹤壁昌宏镁业有限公司",
      "VITE_BASE_API": "http://36.133.39.224:81",
      "VITE_JAVA_API": "http://36.133.39.224:82"
    },
    "logo": "logo/CHMYLogo.png",
    "favicon": "favicon/CHMYfavicon.ico"
  },
  "SJDQ": {
    "env": {
      "VITE_APP_TITLE": "鹤壁市山城区盛久电器有限公司",
      "VITE_BASE_API": "http://36.139.201.235:81",
      "VITE_JAVA_API": "http://36.139.201.235:82"
    },
    "logo": "logo/SJDQLogo.png",
    "favicon": "favicon/SJDQfavicon.ico"
  },
  "TXDQ": {
    "env": {
      "VITE_APP_TITLE": "鹤壁泰信电气有限公司",
      "VITE_BASE_API": "http://36.133.46.171:81",
      "VITE_JAVA_API": "http://36.133.46.171:82"
    },
    "logo": "logo/TXDQLogo.png",
    "favicon": "favicon/TXDQfavicon.ico"
  },
  "HMCL": {
    "env": {
      "VITE_APP_TITLE": "鹤壁浩镁材料有限公司",
      "VITE_BASE_API": "http://36.213.90.123:9522",
      "VITE_JAVA_API": "http://36.213.90.123:9523"
    },
    "logo": "logo/HMCLLogo.png",
    "favicon": "favicon/HMCLfavicon.ico"
  },
  "DTZG": {
    "env": {
@@ -185,7 +248,8 @@
    },
    "logo": "logo/XLZNLogo.png",
    "favicon": "favicon/XLZNfavicon.ico"
  },"SKJS": {
  },
  "SKJS": {
  "env": {
    "VITE_APP_TITLE": "徐州什克金属制品有限公司",
    "VITE_BASE_API": "http://36.213.90.123:9514",
src/api/productionManagement/productionProcess.js
@@ -26,6 +26,14 @@
    params: query,
  });
}
export function listAll() {
  return request({
    url: "/technologyOperation/listPage",
    method: "get",
    params: { current: -1, size: -1 },
  });
}
export function add(data) {
  return request({
    url: "/technologyOperation/add",
src/api/qualityManagement/nonconformingManagement.js
@@ -48,3 +48,12 @@
    data: query,
  });
}
// æŸ¥è¯¢ä¸åˆæ ¼è¯¦æƒ…
export function getQualityUnqualifiedDetail(query) {
  return request({
    url: "/quality/qualityUnqualified/detail/" + query,
    method: "get",
    data: query,
  });
}
src/components/filePreview/index.vue
@@ -1,8 +1,8 @@
<template>
  <el-dialog v-model="dialogVisible" title="预览" width="100%" fullscreen align-center :before-close="handleClose" append-to-body>
  <el-dialog v-model="dialogVisible" title="预览" :width="isImage ? '60%' : '100%'" :fullscreen="!isImage" align-center :before-close="handleClose" append-to-body>
    <div>
      <!-- å›¾ç‰‡é¢„览 -->
      <div v-if="isImage">
      <div v-if="isImage" class="image-preview-container">
        <img :src="imgUrl" alt="Image Preview" />
      </div>
      
@@ -188,8 +188,18 @@
</script>
<style scoped>
.image-preview-container {
  display: flex;
  justify-content: center;
  align-items: center;
  max-height: calc(100vh - 200px);
  width: 100%;
}
img {
  max-width: 100%;
  max-height: calc(100vh - 200px);
  object-fit: contain;
  display: block;
  margin: 0 auto;
}
src/views/basicData/product/ProductSelectDialog.vue
@@ -1,12 +1,18 @@
<template>
  <el-dialog v-model="visible" title="选择产品" width="900px" destroy-on-close :close-on-click-modal="false">
    <el-form :inline="true" :model="query" class="mb-2">
      <el-form-item label="产品大类">
        <el-select v-model="query.topProductParentId" placeholder="选择产品大类" clearable @change="onSearch" style="width: 150px">
          <el-option v-for="item in categoryList" :key="item.id" :label="item.label || item.productName" :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item label="产品名称">
        <el-input v-model="query.productName" placeholder="输入产品名称" clearable @keyup.enter="onSearch" style="width: 200px" />
        <el-input v-model="query.productName" placeholder="输入产品名称" clearable @keyup.enter="onSearch" style="width: 150px" />
      </el-form-item>
      <el-form-item label="产品型号">
        <el-input v-model="query.model" placeholder="输入产品型号" clearable @keyup.enter="onSearch" style="width: 200px" />
        <el-input v-model="query.model" placeholder="输入产品型号" clearable @keyup.enter="onSearch" style="width: 150px" />
      </el-form-item>
      <el-form-item>
@@ -45,6 +51,7 @@
import { computed, onMounted, reactive, ref, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { productModelList, productModelListByUrl } from '@/api/basicData/productModel'
import { productTreeList } from '@/api/basicData/product'
export type ProductRow = {
  id: number;
@@ -70,7 +77,19 @@
const query = reactive({
  productName: "",
  model: "",
  topProductParentId: undefined as number | undefined,
});
const categoryList = ref<any[]>([]);
async function loadCategory() {
  try {
    const res = await productTreeList();
    categoryList.value = res || [];
  } catch (e) {
    console.error("Failed to load product categories", e);
  }
}
const page = reactive({
  pageNum: 1,
@@ -132,6 +151,7 @@
function onReset() {
  query.productName = "";
  query.model = "";
  query.topProductParentId = props.topProductParentId;
  page.pageNum = 1;
  loadData();
}
@@ -162,7 +182,7 @@
      model: query.model.trim(),
      current: page.pageNum,
      size: page.pageSize,
      topProductParentId: props.topProductParentId,
      topProductParentId: query.topProductParentId ?? props.topProductParentId,
    };
    const res: any = props.requestUrl
      ? await productModelListByUrl(props.requestUrl, params)
@@ -179,10 +199,13 @@
watch(() => props.modelValue, (visible) => {
  if (visible) {
    multipleSelection.value = [];
    query.topProductParentId = props.topProductParentId;
  }
});
onMounted(() => {
  loadData()
  query.topProductParentId = props.topProductParentId;
  loadCategory();
  loadData();
})
</script>
src/views/collaborativeApproval/approvalProcess/components/viewDetailDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,630 @@
<template>
  <div>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'approval' ? '审批' : '详情'"
               width="700px"
               @close="closeDia">
      <el-descriptions border :column="1" class="approval-detail">
        <el-descriptions-item label="流程编号">{{ form.approveId }}</el-descriptions-item>
        <el-descriptions-item label="申请部门">{{ form.approveDeptName }}</el-descriptions-item>
        <el-descriptions-item :label="props.approveType == 5 ? '采购合同号' : '审批事由'" v-if="!isQuotationApproval && !isPurchaseApproval">
          {{ form.approveReason }}
        </el-descriptions-item>
        <el-descriptions-item label="报销金额" v-if="Number(props.approveType) === 4">
          {{ form.price }}
        </el-descriptions-item>
      </el-descriptions>
      <!-- æŠ¥ä»·å®¡æ‰¹ï¼šå±•示报价详情(复用销售报价"查看详情对话框"内容结构) -->
      <div v-if="isQuotationApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">报价详情</el-divider>
        <el-skeleton :loading="quotationLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo"
                      description="未查询到对应报价详情" />
            <template v-else>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
                <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
                <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
                <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
                <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="报价总额"
                                      :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    {{ formatCurrency(currentQuotation.totalAmount) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentQuotation.products || []"
                          border
                          style="width: 100%">
                  <el-table-column prop="product"
                                   label="产品名称" />
                  <el-table-column prop="specification"
                                   label="规格型号" />
                  <el-table-column prop="unit"
                                   label="单位" />
                  <el-table-column prop="unitPrice"
                                   label="单价">
                    <template #default="scope">{{ formatCurrency(scope.row.unitPrice) }}</template>
                  </el-table-column>
                </el-table>
              </div>
              <div v-if="currentQuotation.remark"
                   style="margin-top: 20px;">
                <h4>备注</h4>
                <p>{{ currentQuotation.remark }}</p>
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <!-- é‡‡è´­å®¡æ‰¹ï¼šå±•示采购详情 -->
      <div v-if="isPurchaseApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">采购详情</el-divider>
        <el-skeleton :loading="purchaseLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
                      description="未查询到对应采购详情" />
            <template v-else>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="采购合同号">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item>
                <el-descriptions-item label="供应商名称">{{ currentPurchase.supplierName }}</el-descriptions-item>
                <el-descriptions-item label="项目名称">{{ currentPurchase.projectName }}</el-descriptions-item>
                <el-descriptions-item label="销售合同号">{{ currentPurchase.salesContractNo }}</el-descriptions-item>
                <el-descriptions-item label="签订日期">{{ currentPurchase.executionDate }}</el-descriptions-item>
                <el-descriptions-item label="录入日期">{{ currentPurchase.entryDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentPurchase.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="合同金额"
                                      :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    {{ formatCurrency(currentPurchase.contractAmount) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentPurchase.productData || []"
                          border
                          style="width: 100%">
                  <el-table-column prop="productCategory"
                                   label="产品名称" />
                  <el-table-column prop="specificationModel"
                                   label="规格型号" />
                  <el-table-column prop="unit"
                                   label="单位" />
                  <el-table-column prop="quantity"
                                   label="数量" />
                  <el-table-column prop="taxInclusiveUnitPrice"
                                   label="含税单价">
                    <template #default="scope">{{ formatCurrency(scope.row.taxInclusiveUnitPrice) }}</template>
                  </el-table-column>
                  <el-table-column prop="taxInclusiveTotalPrice"
                                   label="含税总价">
                    <template #default="scope">{{ formatCurrency(scope.row.taxInclusiveTotalPrice) }}</template>
                  </el-table-column>
                </el-table>
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <!-- å‘货审批:展示发货详情 -->
      <div v-if="isDeliveryApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">发货详情</el-divider>
        <el-skeleton :loading="deliveryLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentDelivery || !currentDelivery.shippingInfo"
                      description="未查询到对应发货详情" />
            <template v-else>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="销售订单">{{ currentDelivery.shippingInfo.salesContractNo || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货订单号">{{ currentDelivery.shippingInfo.shippingNo || '--' }}</el-descriptions-item>
                <el-descriptions-item label="客户名称">{{ currentDelivery.shippingInfo.customerName || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货类型">{{ currentDelivery.shippingInfo.type || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货日期">{{ currentDelivery.shippingInfo.shippingDate || '--' }}</el-descriptions-item>
                <el-descriptions-item label="审核状态">{{ currentDelivery.shippingInfo.status || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货车牌号">{{ currentDelivery.shippingInfo.shippingCarNumber || '--' }}</el-descriptions-item>
                <el-descriptions-item label="快递公司">{{ currentDelivery.shippingInfo.expressCompany || '--' }}</el-descriptions-item>
                <el-descriptions-item label="快递单号"
                                      :span="2">{{ currentDelivery.shippingInfo.expressNumber || '--' }}</el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="deliveryProductList"
                          border
                          size="small"
                          style="width: 100%">
                  <el-table-column prop="batchNo"
                                   label="批号"
                                   show-overflow-tooltip />
                  <el-table-column prop="productName"
                                   label="产品名称"
                                   show-overflow-tooltip />
                  <el-table-column prop="specificationModel"
                                   label="规格型号"
                                   show-overflow-tooltip />
                  <el-table-column prop="deliveryQuantity"
                                   label="发货数量"
                                   align="center" />
                </el-table>
              </div>
              <div v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
                   style="margin-top: 20px;">
                <h4>发货图片</h4>
                <ImagePreview :file-list="currentDelivery.shippingInfo.storageBlobVOs" />
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <div class="activities-wrapper" style="margin-top: 20px;">
        <el-steps :active="getActiveStep()"
                  finish-status="success"
                  process-status="process"
                  align-center
                  direction="vertical">
          <el-step v-for="(activity, index) in activities"
                   :key="index"
                   finish-status="success"
                   :title="getNodeTitle(index, activities.length)"
                   :description="activity.approveNodeUser"
                   :icon="getNodeIcon(activity, index)">
            <template #icon>
              <el-icon v-if="activity.approveNodeStatus === 2"
                       color="red"
                       :size="22">
                <WarningFilled />
              </el-icon>
              <el-icon v-else-if="activity.isShen"
                       color="#1890ff"
                       :size="22">
                <Edit />
              </el-icon>
              <el-icon v-else-if="activity.approveNodeStatus === 1"
                       color="#67C23A"
                       :size="26">
                <Check />
              </el-icon>
              <el-icon v-else
                       color="#C0C4CC"
                       :size="22">
                <MoreFilled />
              </el-icon>
            </template>
            <template #title>
              <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
            </template>
            <template #description>
              <div class="node-user">
                <div class="avatar-wrapper">
                  <img :src="userStore.avatar"
                       class="user-avatar"
                       alt="" />
                </div>
                <span style="color: #000000">{{ activity.approveNodeUser }}</span>
              </div>
              <div class="node-reason" v-if="activity.approveNodeReason">
                <span>审批意见:</span>{{ activity.approveNodeReason }}
              </div>
            </template>
          </el-step>
        </el-steps>
      </div>
      <el-descriptions border :column="1" style="margin-top: 20px;" class="approval-detail">
        <el-descriptions-item label="附件">
          <ImageUpload v-if="attachments && attachments.length > 0" v-model:file-list="attachments" :disabled="true" />
          <span v-else style="color: #999;">无附件</span>
        </el-descriptions-item>
      </el-descriptions>
      <el-form ref="formRef" :model="form" label-width="100px" style="margin-top: 20px;">
        <el-form-item label="审批意见" prop="approveReason" v-if="operationType === 'approval'">
          <el-input v-model="form.approveReason" type="textarea" placeholder="请输入审批意见" />
        </el-form-item>
      </el-form>
      <template #footer
                v-if="operationType === 'approval'">
        <div class="dialog-footer">
          <el-button type="primary"
                     :loading="submitLoading"
                     @click="submitForm(2)">不通过</el-button>
          <el-button type="primary"
                     :loading="submitLoading"
                     @click="submitForm(1)">通过</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import {
    computed,
    getCurrentInstance,
    nextTick,
    reactive,
    ref,
    toRefs,
  } from "vue";
  import {
    approveProcessDetails,
    getDept,
    updateApproveNode,
  } from "@/api/collaborativeApproval/approvalProcess.js";
  import useUserStore from "@/store/modules/user.js";
  import {
    WarningFilled,
    Edit,
    Check,
    MoreFilled,
  } from "@element-plus/icons-vue";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
  import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js";
  import ImagePreview from "@/components/AttachmentPreview/image/index.vue";
  import { formatCurrency } from "@/utils/numberFormat";
  import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
  import { attachmentList } from "@/api/basicData/storageAttachment.js";
  const emit = defineEmits(["close"]);
  const { proxy } = getCurrentInstance();
  const props = defineProps({
    approveType: {
      type: [Number, String],
      default: 0,
    },
  });
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const activities = ref([]);
  const attachments = ref([]);
  const formRef = ref(null);
  const userStore = useUserStore();
  const productOptions = ref([]);
  const quotationLoading = ref(false);
  const currentQuotation = ref({});
  const purchaseLoading = ref(false);
  const currentPurchase = ref({});
  const deliveryLoading = ref(false);
  const currentDelivery = ref({});
  const deliveryProductList = ref([]);
  const submitLoading = ref(false);
  const isQuotationApproval = computed(() => Number(props.approveType) === 6);
  const isPurchaseApproval = computed(() => Number(props.approveType) === 5);
  const isDeliveryApproval = computed(() => Number(props.approveType) === 7);
  const data = reactive({
    form: {
      approveId: "",
      approveDeptId: "",
      approveReason: "",
      checkResult: "",
    },
  });
  const { form } = toRefs(data);
  // èŠ‚ç‚¹æ ‡é¢˜
  const getNodeTitle = (index, len) => {
    if (index === len - 1) return "结束";
    return "审批";
  };
  // èŽ·å–å½“å‰æ¿€æ´»æ­¥éª¤
  const getActiveStep = () => {
    // å¦‚果所有 isShen éƒ½ä¸º false,返回最后一个步骤(全部完成)
    const hasActive = activities.value.some(a => a.isShen === true);
    if (!hasActive) return activities.value.length;
    // å½“前节点索引
    return activities.value.findIndex(a => a.isShen == true);
  };
  // æ­¥éª¤icon
  const getNodeIcon = (activity, index) => {
    if (activity.approveNodeStatus === 2) return "el-icon-warning"; // ä¸é€šè¿‡
    if (activity.isShen) return "Edit";
    return "";
  };
  // æ‰“开弹框
  const openDialog = (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    currentQuotation.value = {};
    currentPurchase.value = {};
    form.value = { ...row };
    // èŽ·å–é™„ä»¶
    attachments.value = [];
    attachmentList({ recordId: row.id, recordType: 'approve_process' }).then(res => {
      attachments.value = res.data || [];
    });
    // ç«‹å³æ¸…除表单验证状态(因为字段是disabled的,不需要验证)
    nextTick(() => {
      if (formRef.value) {
        formRef.value.clearValidate();
      }
    });
    // ç¡®ä¿é€‰é¡¹åŠ è½½å®ŒæˆåŽå†åŒ¹é…å€¼ç±»åž‹
    getProductOptions().then(() => {
      // ç¡®ä¿å€¼ç±»åž‹åŒ¹é…ï¼ˆå¦‚果选项已加载)
      if (productOptions.value.length > 0 && form.value.approveDeptId) {
        const matchedOption = productOptions.value.find(
          opt =>
            opt.deptId == form.value.approveDeptId ||
            String(opt.deptId) === String(form.value.approveDeptId)
        );
        if (matchedOption) {
          form.value.approveDeptId = matchedOption.deptId;
        }
      }
      // å†æ¬¡æ¸…除验证,确保选项加载后值匹配正确
      nextTick(() => {
        if (formRef.value) {
          formRef.value.clearValidate();
        }
      });
    });
    // æŠ¥ä»·å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"报价单号"去查报价列表
    if (isQuotationApproval.value) {
      const quotationNo = row?.approveReason;
      if (quotationNo) {
        quotationLoading.value = true;
        getQuotationList({ quotationNo })
          .then(res => {
            const records = res?.data?.records || [];
            currentQuotation.value = records[0] || {};
          })
          .finally(() => {
            quotationLoading.value = false;
          });
      }
    }
    // é‡‡è´­å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"采购合同号"去查采购详情
    if (isPurchaseApproval.value) {
      const purchaseContractNumber = row?.approveReason;
      if (purchaseContractNumber) {
        purchaseLoading.value = true;
        getPurchaseByCode({ purchaseContractNumber })
          .then(res => {
            currentPurchase.value = res;
          })
          .catch(err => {
            console.error("查询采购详情失败:", err);
            proxy.$modal.msgError("查询采购详情失败");
          })
          .finally(() => {
            purchaseLoading.value = false;
          });
      }
    }
    // å‘货审批:用审批事由字段承载的"发货单号"去查发货详情
    if (isDeliveryApproval.value) {
      const deliveryNo = row?.approveReason;
      if (deliveryNo) {
        deliveryLoading.value = true;
        currentDelivery.value = {};
        deliveryProductList.value = [];
        getDeliveryDetailByShippingNo({ shippingNo: deliveryNo })
          .then(res => {
            const detailData = res?.data || res || {};
            currentDelivery.value = detailData;
            deliveryProductList.value =
              detailData.shippingProductDetailDtoList || [];
          })
          .catch(err => {
            console.error("查询发货详情失败:", err);
            proxy.$modal.msgError("查询发货详情失败");
          })
          .finally(() => {
            deliveryLoading.value = false;
          });
      }
    }
    approveProcessDetails(row.approveId).then(res => {
      activities.value = res.data;
      // å¢žåŠ isApproval字段
      activities.value.forEach(item => {
        if (item.url && item.url.includes("word")) {
          item.urlTem = item.url.replaceAll("word", "img");
        } else {
          item.urlTem = item.url;
        }
        if (item.approveNodeStatus === 2) {
          item.isApproval = "已驳回";
        } else if (item.approveNodeStatus === 1) {
          item.isApproval = "已同意";
        } else {
          item.isApproval = "未审批";
        }
      });
    });
  };
  const getDeliveryProductInfoList = () => {
    const row = currentDelivery.value;
    if (!row) return [];
    const normalizeBatchNoList = value => {
      if (Array.isArray(value)) return value;
      if (typeof value === "string" && value.includes(",")) {
        return value
          .split(",")
          .map(item => item.trim())
          .filter(Boolean);
      }
      return value ? [value] : [];
    };
    const detailList = deliveryProductList.value.length
      ? deliveryProductList.value
      : [
          row.batchNoDetailList,
          row.batchNoList,
          row.shippingBatchList,
          row.shippingInfoDetailList,
          row.detailList,
          row.batchDetailList,
        ].find(value => Array.isArray(value) && value.length);
    const batchNoList = normalizeBatchNoList(row.batchNo);
    const toTableRow = (item = {}) => ({
      batchNo:
        typeof item === "string" || typeof item === "number"
          ? item
          : item.batchNo ?? item.batchNumber ?? row.batchNo ?? "--",
      productName: item.productName ?? row.productName ?? "--",
      specificationModel:
        item.specificationModel ?? item.model ?? row.specificationModel ?? "--",
      deliveryQuantity:
        item.deliveryQuantity ??
        item.quantity ??
        item.shippingQuantity ??
        row.deliveryQuantity ??
        row.quantity ??
        "--",
    });
    if (detailList?.length) {
      return detailList.map(toTableRow);
    }
    if (batchNoList.length) {
      return batchNoList.map(batchNo => toTableRow({ batchNo }));
    }
    return [toTableRow()];
  };
  const getApprovalStatusText = status => {
    const statusMap = {
      0: "待审核",
      1: "审核通过",
      2: "审核拒绝",
      3: "审核中",
    };
    return statusMap[status] || "待审核";
  };
  const getProductOptions = () => {
    return getDept().then(res => {
      productOptions.value = res.data;
    });
  };
  // æäº¤å®¡æ‰¹
  const submitForm = status => {
    const filteredActivities = activities.value.filter(
      activity => activity.isShen
    );
    if (!filteredActivities || filteredActivities.length === 0) {
      proxy.$modal.msgError("未找到待审批的节点");
      return;
    }
    const currentActivity = filteredActivities[0];
    if (!currentActivity) {
      proxy.$modal.msgError("未找到待审批的节点");
      return;
    }
    currentActivity.approveNodeStatus = status;
    // åˆ¤æ–­æ˜¯å¦ä¸ºæœ€åŽä¸€æ­¥
    const isLast =
      activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
    submitLoading.value = true;
    updateApproveNode({ ...currentActivity, isLast })
      .then(() => {
        proxy.$modal.msgSuccess("提交成功");
        closeDia();
      })
      .finally(() => {
        submitLoading.value = false;
      });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    quotationLoading.value = false;
    currentQuotation.value = {};
    purchaseLoading.value = false;
    currentPurchase.value = {};
    submitLoading.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
  .node-user {
    margin: 10px 0;
    font-size: 16px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .node-status {
    color: #1890ff;
    margin-left: 8px;
    font-size: 14px;
  }
  .node-reason {
    font-size: 15px;
    color: #333;
    margin: 10px 0;
  }
  .user-avatar {
    cursor: pointer;
    width: 30px;
    height: 30px;
    border-radius: 50px;
  }
  .signImg {
    cursor: pointer;
    width: 200px;
    height: 60px;
  }
  .approval-detail :deep(.el-upload) {
    display: none !important;
  }
  .approval-detail :deep(.upload-tip) {
    display: none !important;
  }
</style>
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -124,8 +124,9 @@
    </el-card>
    <!-- å¼¹çª—组件 -->
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
    <info-form-dia ref="infoFormDia" @close="handleDialogClose" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleDialogClose" :approveType="currentApproveType"></approval-dia>
    <view-detail-dia ref="viewDetailDia" @close="handleDialogClose" :approveType="currentApproveType"></view-detail-dia>
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              record-type="approve_process"
@@ -140,6 +141,7 @@
import { useRoute } from 'vue-router';
import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
import ViewDetailDia from "@/views/collaborativeApproval/approvalProcess/components/viewDetailDia.vue";
import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
@@ -393,12 +395,17 @@
});
const infoFormDia = ref()
const approvalDia = ref()
const viewDetailDia = ref()
const { proxy } = getCurrentInstance()
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
};
const handleDialogClose = () => {
  getList();
};
@@ -425,6 +432,10 @@
    page.total = res.data.total;
    // æ›´æ–°å½“前类型数量
    typeCounts.value[activeTab.value] = res.data.total;
    if (res.data.records.length === 0 && page.current > 1) {
      page.current = Math.max(1, Math.ceil(res.data.total / page.size));
      getList();
    }
  }).catch(err => {
    tableLoading.value = false;
  })
@@ -470,7 +481,11 @@
// æ‰“开新增检验弹框
const openApprovalDia = (type, row) => {
  nextTick(() => {
    if (type === 'view') {
      viewDetailDia.value?.openDialog(type, row)
    } else {
    approvalDia.value?.openDialog(type, row)
    }
  })
};
src/views/equipmentManagement/ledger/index.vue
@@ -115,10 +115,10 @@
      <el-descriptions :column="2" border>
        <el-descriptions-item label="设备名称">{{ detailData.deviceName }}</el-descriptions-item>
        <el-descriptions-item label="规格型号">{{ detailData.deviceModel }}</el-descriptions-item>
        <el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item>
        <!-- <el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item> -->
        <el-descriptions-item label="设备类型">{{ detailData.type }}</el-descriptions-item>
        <el-descriptions-item label="供应商">{{ detailData.supplierName }}</el-descriptions-item>
        <el-descriptions-item label="存放位置">{{ detailData.storageLocation }}</el-descriptions-item>
        <!-- <el-descriptions-item label="存放位置">{{ detailData.storageLocation }}</el-descriptions-item> -->
        <el-descriptions-item label="单位">{{ detailData.unit }}</el-descriptions-item>
        <el-descriptions-item label="数量">{{ detailData.number }}</el-descriptions-item>
        <el-descriptions-item label="启用折旧">{{ detailData.isDepr === 1 ? '是' : '否' }}</el-descriptions-item>
@@ -219,10 +219,10 @@
      label: "规格型号",
      prop: "deviceModel",
    },
    {
      label: "设备品牌",
      prop: "deviceBrand",
    },
    // {
    //   label: "设备品牌",
    //   prop: "deviceBrand",
    // },
    {
      label: "设备类型",
      prop: "type",
@@ -231,10 +231,10 @@
      label: "供应商",
      prop: "supplierName",
    },
    {
      label: "存放位置",
      prop: "storageLocation",
    },
    // {
    //   label: "存放位置",
    //   prop: "storageLocation",
    // },
    {
      label: "数量",
      prop: "number",
src/views/equipmentManagement/repair/Modal/RepairDetailModal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<template>
  <el-dialog
    v-model="visible"
    title="设备报修详情"
    width="800px"
    destroy-on-close
    :close-on-click-modal="false"
    @close="handleClose"
  >
    <el-descriptions border :column="2" class="repair-detail">
      <el-descriptions-item label="设备名称">{{ form.deviceName }}</el-descriptions-item>
      <el-descriptions-item label="规格型号">{{ form.deviceModel }}</el-descriptions-item>
      <el-descriptions-item label="报修日期">
        {{ form.repairTime ? dayjs(form.repairTime).format("YYYY-MM-DD") : "" }}
      </el-descriptions-item>
      <el-descriptions-item label="报修人">{{ form.repairName }}</el-descriptions-item>
      <el-descriptions-item label="报修项目">{{ form.machineryCategory }}</el-descriptions-item>
      <el-descriptions-item label="维修人">{{ form.maintenanceName }}</el-descriptions-item>
      <el-descriptions-item label="报修状态">
        <el-tag v-if="form.status === 2" type="danger">失败</el-tag>
        <el-tag v-else-if="form.status === 1" type="success">已验收</el-tag>
        <el-tag v-else-if="form.status === 3" type="info">待验收</el-tag>
        <el-tag v-else-if="form.status === 0" type="warning">待维修</el-tag>
      </el-descriptions-item>
      <el-descriptions-item label="故障现象">{{ form.remark }}</el-descriptions-item>
      <!-- éªŒæ”¶ä¿¡æ¯ -->
      <template v-if="form.status === 1 || form.status === 3">
        <el-descriptions-item label="验收人">{{ form.acceptanceName }}</el-descriptions-item>
        <el-descriptions-item label="验收时间">{{ form.acceptanceTime }}</el-descriptions-item>
        <el-descriptions-item label="验收备注" :span="2">{{ form.acceptanceRemark }}</el-descriptions-item>
      </template>
      <!-- é™„ä»¶ -->
      <el-descriptions-item label="附件" :span="2">
        <ImageUpload
          v-if="form.storageBlobDTOs && form.storageBlobDTOs.length > 0"
          v-model:file-list="form.storageBlobDTOs"
          :disabled="true"
        />
        <span v-else style="color: #999;">无附件</span>
      </el-descriptions-item>
    </el-descriptions>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">关闭</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, nextTick } from "vue";
import dayjs from "dayjs";
import { getRepairById } from "@/api/equipmentManagement/repair";
import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
const visible = ref(false);
const form = ref({});
const openView = async (id) => {
  visible.value = true;
  await nextTick();
  try {
    const { data } = await getRepairById(id);
    // ç¡®ä¿é™„件字段名称匹配组件需要
    data.storageBlobDTOs = data.storageBlobVOs || [];
    form.value = data || {};
  } catch (error) {
    console.error("Failed to load repair details:", error);
  }
};
const handleClose = () => {
  visible.value = false;
  form.value = {};
};
defineExpose({
  openView,
});
</script>
<style scoped>
.repair-detail :deep(.el-descriptions__label) {
  width: 120px;
  justify-content: flex-end;
}
.repair-detail :deep(.el-upload) {
  display: none !important;
}
</style>
src/views/equipmentManagement/repair/index.vue
@@ -136,6 +136,7 @@
    </div>
    <RepairModal ref="repairModalRef"
                 @ok="getTableData" />
    <RepairDetailModal ref="repairDetailModalRef" />
    <MaintainModal ref="maintainModalRef"
                   @ok="getTableData" />
    <AcceptanceModal ref="acceptanceModalRef"
@@ -158,6 +159,7 @@
  import { usePaginationApi } from "@/hooks/usePaginationApi";
  import { getRepairPage, delRepair } from "@/api/equipmentManagement/repair";
  import RepairModal from "./Modal/RepairModal.vue";
  import RepairDetailModal from "./Modal/RepairDetailModal.vue";
  import { ElMessageBox, ElMessage } from "element-plus";
  import dayjs from "dayjs";
  import MaintainModal from "./Modal/MaintainModal.vue";
@@ -174,6 +176,7 @@
  // æ¨¡æ€æ¡†å®žä¾‹
  const repairModalRef = ref();
  const repairDetailModalRef = ref();
  const maintainModalRef = ref();
  const acceptanceModalRef = ref();
@@ -274,14 +277,14 @@
    return multipleList.value.some(item => item.status === 1);
  });
  // æ–°å¢žæŠ¥ä¿®
  // æ–°å¢žè®¾å¤‡æŠ¥ä¿®
  const addRepair = () => {
    repairModalRef.value.openAdd();
  };
  // è¯¦æƒ…查看
  // æŸ¥çœ‹è¯¦æƒ…
  const viewRepair = id => {
    repairModalRef.value.openView(id);
    repairDetailModalRef.value.openView(id);
  };
  // ç¼–辑报修
src/views/inventoryManagement/stockManagement/New.vue
@@ -52,8 +52,9 @@
        <el-form-item label="库存数量"
                      prop="qualitity">
          <el-input-number v-model="formState.qualitity"
                           :step="1"
                           :min="1"
                           :step="0.00000001"
                           :min="0"
                           :precision="8"
                           style="width: 100%" />
        </el-form-item>
        <el-form-item label="批号"
@@ -65,8 +66,9 @@
                      label="库存预警数量"
                      prop="warnNum">
          <el-input-number v-model="formState.warnNum"
                           :step="1"
                           :step="0.00000001"
                           :min="0"
                           :precision="8"
                           :max="formState.qualitity"
                           style="width: 100%" />
        </el-form-item>
@@ -119,7 +121,7 @@
    productName: "",
    productModelName: "",
    unit: "",
    type: undefined,
    type: "qualified",
    qualitity: 0,
    batchNo: null,
    warnNum: 0,
@@ -157,7 +159,7 @@
      productName: "",
      productModelName: "",
      unit: "",
      type: undefined,
      type: "qualified",
      qualitity: 0,
      batchNo: null,
      warnNum: 0,
src/views/monitor/logininfor/index.vue
@@ -95,7 +95,11 @@
      <el-table ref="logininforRef" v-loading="loading" :data="logininforList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="访问编号" align="center" prop="infoId" />
         <el-table-column label="访问编号" align="center">
            <template #default="scope">
               <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
            </template>
         </el-table-column>
         <el-table-column label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
         <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
         <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
src/views/monitor/operlog/index.vue
@@ -109,7 +109,11 @@
      <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
         <el-table-column type="selection" width="50" align="center" />
         <el-table-column label="日志编号" align="center" prop="operId" />
         <el-table-column label="日志编号" align="center">
            <template #default="scope">
               <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
            </template>
         </el-table-column>
         <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
         <el-table-column label="操作类型" align="center" prop="businessType">
            <template #default="scope">
@@ -145,6 +149,7 @@
         :total="total"
         v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize"
         :page-sizes="[10, 20, 30, 50, 100, 200]"
         @pagination="getList"
      />
src/views/procurementManagement/paymentLedger/index.vue
@@ -202,6 +202,14 @@
    page.size = obj.limit;
    getList();
  };
  // å°†æœªå…¥åº“金额并入已入库金额,未入库金额置0
  const mergeUnshippedAmount = row => {
    if (row && row.unshippedAmount) {
      row.shippedAmount = (Number(row.shippedAmount) || 0) + (Number(row.unshippedAmount) || 0);
      row.unshippedAmount = 0;
    }
    return row;
  };
  const getList = () => {
    tableLoading.value = true;
    paymentLedgerList({
@@ -210,7 +218,7 @@
    }).then(res => {
      let result = res.data;
      tableLoading.value = false;
      tableData.value = result.records || [];
      tableData.value = (result.records || []).map(mergeUnshippedAmount);
      total.value = result.total || 0;
      if (tableData.value.length > 0) {
        currentSupplierId.value = tableData.value[0].supplierId;
@@ -231,11 +239,11 @@
        tableLoadingSon.value = false;
        let result = res.data;
        if (Array.isArray(result)) {
          tableDataSon.value = result;
          tableDataSon.value = result.map(mergeUnshippedAmount);
          sonPage.total = result.length;
          handlePagination({ page: sonPage.current, limit: sonPage.size });
        } else {
          originalTableDataSon.value = result.records || [];
          originalTableDataSon.value = (result.records || []).map(mergeUnshippedAmount);
          sonPage.total = result.total || 0;
        }
      })
src/views/procurementManagement/procurementLedger/index.vue
@@ -90,7 +90,7 @@
        <el-table-column label="入库状态" prop="stockInStatus" width="100" show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="getStockInStatusType(scope.row.stockInStatus)" size="small">
              {{ scope.row.stockInStatus || '--' }}
              {{ formatStockInStatus(scope.row.stockInStatus) || '--' }}
            </el-tag>
          </template>
        </el-table-column>
@@ -137,7 +137,7 @@
          <el-col :span="12">
            <el-form-item label="供应商名称:" prop="supplierId">
              <el-select v-model="form.supplierId" placeholder="请选择" filterable clearable>
                <el-option v-for="item in supplierList" :key="item.id" :label="item.supplierName" :value="item.id">{{ item.supplierType ? item.supplierName + '---' + item.supplierType : item.supplierName }}</el-option>
<el-option v-for="item in supplierList" :key="item.id" :label="item.supplierName" :value="item.id">{{ item.supplierName }}</el-option>
              </el-select>
            </el-form-item>
          </el-col>
@@ -480,11 +480,17 @@
  return typeMap[status] || "";
};
// å…¥åº“状态显示:入库中也展示为完全入库
const formatStockInStatus = status => {
  if (status === "入库中") return "完全入库";
  return status;
};
// èŽ·å–å…¥åº“çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getStockInStatusType = status => {
  const typeMap = {
    "待入库": "info", // å¾…入库 - ç°è‰²
    "入库中": "warning", // å…¥åº“中 - æ©™è‰²
    "入库中": "success", // å…¥åº“中 - ç»¿è‰²
    "完全入库": "success", // å®Œå…¨å…¥åº“ - ç»¿è‰²
  };
  return typeMap[status] || "";
src/views/procurementManagement/procurementPlan/index.vue
@@ -183,7 +183,7 @@
          <div class="checkbox-group">
            <el-checkbox v-model="formData.summaryMaterial">物料</el-checkbox>
            <el-checkbox v-model="formData.summaryAuxAttributes">辅助属性</el-checkbox>
            <el-checkbox v-model="formData.summaryDemandDate">需求日期</el-checkbox>
            <el-checkbox v-model="formData.summaryDemandDate">交货日期</el-checkbox>
          </div>
        </div>
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -343,7 +343,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     @change="handleUnitQuantityChange"
                                     @change="handleUnitQuantityChange(row)"
                                     :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
                  </el-form-item>
                </template>
@@ -1196,52 +1196,6 @@
    });
  };
  const toQuantityNumber = value => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(4));
  };
  const syncDemandedQuantityTree = (items, parentDemandedQuantity = null) => {
    items.forEach(item => {
      if (parentDemandedQuantity !== null) {
        item.demandedQuantity = toQuantityNumber(
          parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
        );
      }
      if (Array.isArray(item.children) && item.children.length > 0) {
        syncDemandedQuantityTree(
          item.children,
          toQuantityNumber(item.demandedQuantity)
        );
      }
    });
  };
  const recalculateDemandedQuantities = () => {
    if (pageType.value !== "order") {
      return;
    }
    const rootDemandedQuantity = routeInfo.value.quantity;
    if (
      rootDemandedQuantity === undefined ||
      rootDemandedQuantity === null ||
      rootDemandedQuantity === ""
    ) {
      syncDemandedQuantityTree(bomDataValue.value.dataList);
      return;
    }
    syncDemandedQuantityTree(
      bomDataValue.value.dataList,
      toQuantityNumber(rootDemandedQuantity)
    );
  };
  const processChange = value => {
    processOptions.value.forEach(item => {
      if (item.id == value) {
@@ -1266,6 +1220,24 @@
      }
    }
    return null;
  };
  const toQuantityNumber = value => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(4));
  };
  const handleUnitQuantityChange = row => {
    if (pageType.value !== "order") {
      return;
    }
    row.demandedQuantity = toQuantityNumber(
      toQuantityNumber(row.unitQuantity) * toQuantityNumber(routeInfo.value.quantity)
    );
  };
  const handleBomProcessChange = (row, value) => {
@@ -1301,7 +1273,6 @@
      );
      bomDataValue.value.dataList = data || [];
      normalizeTreeData(bomDataValue.value.dataList);
      recalculateDemandedQuantities();
    } catch (err) {
      console.error("获取BOM数据失败:", err);
    }
@@ -1397,10 +1368,6 @@
    });
  };
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
  const addchildItem = (item, tempId) => {
    if (item.tempId === tempId) {
      if (!item.children) {
@@ -1424,7 +1391,6 @@
        unit: "",
        tempId: new Date().getTime(),
      });
      recalculateDemandedQuantities();
      return true;
    }
    if (item.children && item.children.length > 0) {
@@ -1461,7 +1427,6 @@
          children: [],
          tempId: new Date().getTime(),
        });
        recalculateDemandedQuantities();
        return;
      }
      addchildItem(item, tempId);
@@ -1556,7 +1521,6 @@
    console.log(bomDataValue.value.dataList, "bomDataValue.value.dataList");
    normalizeTreeData(bomDataValue.value.dataList);
    recalculateDemandedQuantities();
    const valid = validateAllBom();
    if (valid) {
src/views/productionManagement/productStructure/Detail/index.vue
@@ -22,16 +22,34 @@
              border
              :preserve-expanded-content="false"
              :default-expand-all="true"
              style="width: 100%">
              style="width: 100%"
              @expand-change="handleOuterExpandChange">
      <el-table-column type="expand">
        <template #default="props">
          <el-form ref="form"
                   :model="dataValue">
            <el-table :data="dataValue.dataList"
            <div v-if="dataValue.isEdit"
                 class="drag-tip">
              <el-icon><Rank /></el-icon>
              <span>拖动手柄调整结构:同层级直接拖动排序;拖到某行正下方成为其子级;拖到列表最前成为一级</span>
            </div>
            <el-table ref="tableRef"
                      :data="dataValue.dataList"
                      row-key="tempId"
                      default-expand-all
                      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
                      @expand-change="handleTreeExpandChange"
                      style="width: 100%">
              <el-table-column v-if="dataValue.isEdit"
                               width="44"
                               align="center">
                <template #default="{ row }">
                  <div class="drag-handle"
                       title="拖动调整顺序:拖到某行正下方成为其子级,拖到列表最前成为一级">
                    <el-icon><Rank /></el-icon>
                  </div>
                </template>
              </el-table-column>
              <el-table-column prop="productName"
                               label="产品" />
              <el-table-column prop="model"
@@ -59,18 +77,20 @@
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
                               filterable
                    <el-input v-model="row.processName"
                              readonly
                              placeholder="请选择消耗工序"
                               clearable
                               style="width: 100%"
                               @change="value => handleProcessChange(row, value)"
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
                      <el-option v-for="item in dataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
                                 :value="item.id" />
                    </el-select>
                              :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                              @click="openProcessDialog(row, $event)"
                              @clear="clearProcess(row)">
                      <template #append>
                        <el-button icon="Search"
                                   :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                                   @click="openProcessDialog(row)" />
                      </template>
                    </el-input>
                  </el-form-item>
                </template>
              </el-table-column>
@@ -150,6 +170,9 @@
                           v-model:model-value="dataValue.showProductDialog"
                           :single="true"
                           @confirm="handleProduct" />
    <process-select-dialog v-if="dataValue.showProcessDialog"
                           v-model:model-value="dataValue.showProcessDialog"
                           @confirm="handleProcessConfirm" />
  </div>
</template>
@@ -158,18 +181,23 @@
    computed,
    defineAsyncComponent,
    defineComponent,
    nextTick,
    onBeforeUnmount,
    onMounted,
    reactive,
    ref,
    watch,
  } from "vue";
  import {
    queryList,
    addBomDetail,
  } from "@/api/productionManagement/productStructure.js";
  import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
  import { list } from "@/api/productionManagement/productionProcess";
  import { listAll } from "@/api/productionManagement/productionProcess";
  import { ElMessage } from "element-plus";
  import { useRoute, useRouter } from "vue-router";
  import Sortable from "sortablejs";
  import { Rank } from "@element-plus/icons-vue";
  defineComponent({
    name: "StructureEdit",
@@ -178,8 +206,16 @@
  const ProductSelectDialog = defineAsyncComponent(
    () => import("@/views/basicData/product/ProductSelectDialog.vue")
  );
  const ProcessSelectDialog = defineAsyncComponent(
    () =>
      import(
        "@/views/productionManagement/productionProcess/ProcessSelectDialog.vue"
      )
  );
  const emit = defineEmits(["update:router"]);
  const form = ref();
  const tableRef = ref();
  let tableSortable = null;
  const route = useRoute();
  const router = useRouter();
@@ -210,8 +246,10 @@
    productOptions: [],
    processOptions: [],
    showProductDialog: false,
    showProcessDialog: false,
    currentRowIndex: null,
    currentRowName: null,
    currentProcessRow: null,
    loading: false,
    isEdit: false,
  });
@@ -303,6 +341,155 @@
    syncDemandedQuantityTree(dataValue.dataList);
  };
  // ===== æ‹–拽排序 =====
  // æŒ‰å½“前 DOM è¡Œé¡ºåºå±•开成扁平列表(编辑模式下强制全部展开,保证行顺序与数据一致)
  const buildFlatTree = () => {
    const flat = [];
    const walk = (items, depth, parentNode) => {
      items.forEach(node => {
        flat.push({ node, depth, parentArray: items, parentNode });
        if (Array.isArray(node.children) && node.children.length > 0) {
          walk(node.children, depth + 1, node);
        }
      });
    };
    walk(dataValue.dataList, 0, null);
    return flat;
  };
  const removeNodeByTempId = (items, tempId) => {
    for (let i = 0; i < items.length; i++) {
      if (String(items[i].tempId) === String(tempId)) {
        items.splice(i, 1);
        return true;
      }
      if (
        Array.isArray(items[i].children) &&
        removeNodeByTempId(items[i].children, tempId)
      ) {
        return true;
      }
    }
    return false;
  };
  const updateParentRef = (node, parent) => {
    node.parentTempId = parent?.tempId ?? "";
    node.parentId = parent?.id ?? "";
  };
  const handleSortEnd = evt => {
    const { oldIndex, newIndex } = evt;
    if (
      typeof oldIndex !== "number" ||
      typeof newIndex !== "number" ||
      oldIndex === newIndex
    ) {
      return;
    }
    const flat = buildFlatTree();
    if (oldIndex < 0 || oldIndex >= flat.length) return;
    const dragged = flat[oldIndex];
    // æ‹–放后位于被拖行上方一行的数据(按最终 DOM é¡ºåºæ¢ç®—到原扁平列表)
    let aEntry = null;
    if (newIndex > oldIndex) {
      aEntry = flat[newIndex];
    } else if (newIndex > 0) {
      aEntry = flat[newIndex - 1];
    }
    if (!aEntry) {
      // æ‹–到列表最前 â†’ æˆä¸ºä¸€çº§
      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
      dataValue.dataList.unshift(dragged.node);
      updateParentRef(dragged.node, null);
    } else if (aEntry.depth < dragged.depth) {
      // æ‹–到某行正下方 â†’ æˆä¸ºè¯¥è¡Œçš„子级
      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
      if (!aEntry.node.children) {
        aEntry.node.children = [];
      }
      aEntry.node.children.push(dragged.node);
      updateParentRef(dragged.node, aEntry.node);
    } else {
      // åŒçº§æ’å…¥:向上找第一个深度不超过被拖行的节点
      let i = flat.indexOf(aEntry);
      while (i >= 0 && flat[i].depth > dragged.depth) {
        i--;
      }
      const eEntry = flat[i];
      // æ‹–回自己的子树内 â†’ ä¸å¤„理(防止形成循环)
      if (!eEntry || eEntry.node.tempId === dragged.node.tempId) {
        return;
      }
      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
      const siblings = eEntry.parentArray;
      const idx = siblings.indexOf(eEntry.node);
      siblings.splice(idx + 1, 0, dragged.node);
      updateParentRef(dragged.node, eEntry.parentNode);
    }
    recalculateDemandedQuantities();
  };
  const initSortable = () => {
    if (!tableRef.value) return;
    const tbody = tableRef.value.$el.querySelector(".el-table__body tbody");
    if (!tbody) return;
    tableSortable?.destroy();
    tableSortable = new Sortable(tbody, {
      animation: 150,
      handle: ".drag-handle",
      disabled: !dataValue.isEdit,
      ghostClass: "sortable-ghost",
      onEnd: handleSortEnd,
    });
  };
  const expandAllTree = () => {
    const table = tableRef.value;
    if (!table) return;
    const walk = items => {
      items.forEach(item => {
        if (Array.isArray(item.children) && item.children.length > 0) {
          table.toggleRowExpansion(item, true);
          walk(item.children);
        }
      });
    };
    walk(dataValue.dataList);
  };
  // ç¼–辑模式下不允许折叠,保证拖拽时 DOM è¡Œé¡ºåºä¸Ž dataList ä¸€è‡´
  const handleTreeExpandChange = (row, expanded) => {
    if (dataValue.isEdit && expanded === false) {
      nextTick(() => {
        tableRef.value?.toggleRowExpansion(row, true);
      });
    }
  };
  // å¤–层行折叠后内容会销毁,重新展开时重新初始化拖拽
  const handleOuterExpandChange = (row, expandedRows) => {
    if (Array.isArray(expandedRows) && expandedRows.some(r => r === row)) {
      nextTick(() => initSortable());
    }
  };
  watch(
    () => dataValue.isEdit,
    val => {
      tableSortable?.option("disabled", !val);
      if (val) {
        nextTick(() => {
          initSortable();
          expandAllTree();
        });
      }
    }
  );
  const buildSubmitTree = (items: any[]) => {
    return items.map((item: any) => {
      const current = { ...item };
@@ -346,6 +533,35 @@
    }
  };
  const openProcessDialog = (row: any, event?: Event) => {
    // ç‚¹å‡»æ¸…除按钮时不弹窗
    const target = event?.target as HTMLElement | undefined;
    if (target && target.closest(".el-input__clear")) {
      return;
    }
    dataValue.currentProcessRow = row;
    dataValue.showProcessDialog = true;
  };
  const clearProcess = (row: any) => {
    row.processId = "";
    row.operationId = "";
    row.processName = "";
    row.operationName = "";
  };
  const handleProcessConfirm = (processRow: any) => {
    const row = dataValue.currentProcessRow;
    if (!row) {
      return;
    }
    row.processId = processRow.id;
    row.operationId = processRow.id;
    row.processName = processRow.name;
    row.operationName = processRow.name;
    handleProcessChange(row, processRow.id);
  };
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
@@ -382,7 +598,7 @@
  };
  const fetchProcessOptions = async () => {
    const { data } = await list({});
    const { data } = await listAll();
    console.log(data, "dataValue.dataList");
    dataValue.processOptions = normalizeListData(data);
  };
@@ -693,5 +909,38 @@
    // å…ˆåŠ è½½å·¥åºé€‰é¡¹ï¼Œå†åŠ è½½æ•°æ®ï¼Œç¡®ä¿el-select能够正确回显
    await fetchProcessOptions();
    await fetchData();
    await nextTick();
    initSortable();
  });
  onBeforeUnmount(() => {
    tableSortable?.destroy();
    tableSortable = null;
  });
</script>
<style scoped>
  .drag-tip {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-bottom: 8px;
    font-size: 12px;
    color: #909399;
  }
  .drag-handle {
    cursor: move;
    color: #909399;
    text-align: center;
  }
  .drag-handle:hover {
    color: var(--el-color-primary);
  }
  :deep(.sortable-ghost) {
    opacity: 0.6;
    background-color: #f5f7fa !important;
  }
</style>
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -2,18 +2,25 @@
  <div>
    <el-dialog v-model="dialogVisible"
               title="领料台账"
               width="1350px"
               width="1450px"
               @close="handleClose">
      <div class="material-toolbar">
        <el-button type="primary"
                   plain
                   @click="handleAddPurchase">新增采购</el-button>
        <el-button type="primary"
                   @click="handleAddMaterialRow">新增</el-button>
      </div>
      <el-table v-loading="materialTableLoading"
                :data="materialTableData"
                border
                @selection-change="handleMaterialSelectionChange"
                row-key="tempId">
        <el-table-column align="center"
                         type="selection"
                         width="55" />
        <el-table-column label="工序名称"
                         min-width="80">
                         min-width="100">
          <template #default="{ row }">
            <span v-if="row.bom === true">{{ row.operationName || "-" }}</span>
            <el-select v-else
@@ -43,13 +50,13 @@
          </template>
        </el-table-column>
        <el-table-column label="原料型号"
                         min-width="120">
                         min-width="110">
          <template #default="{ row }">
            {{ row.materialModel || "-" }}
          </template>
        </el-table-column>
        <!-- æ‰¹å·å¤šé€‰ -->
        <el-table-column min-width="200"
        <el-table-column min-width="180"
                         label="批号">
          <template #default="{ row }">
            <el-select v-model="row.batchNo"
@@ -66,7 +73,7 @@
          </template>
        </el-table-column>
        <el-table-column label="需求数量"
                         min-width="100">
                         min-width="95">
          <template #default="{ row }">
            <span v-if="row.bom === true">{{ stripTrailingZeros(row.demandedQuantity) ?? "-" }}</span>
            <el-input-number v-else
@@ -78,6 +85,27 @@
                             :parser="value => parseFloat(value) || 0"
                             style="width: 100%;"
                             @change="val => handleRequiredQtyChange(row, val)" />
          </template>
        </el-table-column>
        <el-table-column label="库存余量"
                         min-width="90">
          <template #default="{ row }">
            <span v-if="row.stockQty === null">-</span>
            <span v-else>{{ stripTrailingZeros(row.stockQty) }}</span>
          </template>
        </el-table-column>
        <el-table-column label="已领数量"
                         min-width="90">
          <template #default="{ row }">
            {{ stripTrailingZeros(row.pickedQty) ?? 0 }}
          </template>
        </el-table-column>
        <el-table-column label="剩余可领"
                         min-width="90">
          <template #default="{ row }">
            <span :class="{ 'remaining-empty': Number(row.remainingQty) <= 0 }">
              {{ stripTrailingZeros(row.remainingQty) ?? 0 }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="计量单位"
@@ -102,7 +130,7 @@
                         width="90"
                         fixed="right">
          <template #default="{ $index, row }">
            <el-button v-if="row.bom !== true"
            <el-button v-if="row.bom !== true || Number(row.pickQty) > 0"
                       type="danger"
                       link
                       @click="handleDeleteMaterialRow($index)">删除</el-button>
@@ -122,6 +150,8 @@
    <ProductSelectDialog v-model="materialProductDialogVisible"
                         @confirm="handleMaterialProductConfirm"
                         single />
    <PurchaseLedgerAddDialog ref="purchaseLedgerAddDialogRef"
                             v-model="purchaseLedgerDialogVisible" />
    <!-- request-url="/stockInventory/rawMaterials" -->
  </div>
</template>
@@ -130,18 +160,14 @@
  import { computed, ref, watch } from "vue";
  import { ElMessage } from "element-plus";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import {
    findProductProcessRouteItemList,
    listMain,
  } from "@/api/productionManagement/productProcessRoute.js";
  import PurchaseLedgerAddDialog from "./PurchaseLedgerAddDialog.vue";
  import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js";
  import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
  import {
    listMaterialPickingDetail,
    listMaterialPickingBom,
    listMaterialPickingLedger,
    saveMaterialPickingLedger,
    updateMaterialPickingLedger,
  } from "@/api/productionManagement/productionOrder.js";
  import { queryList2 } from "@/api/productionManagement/productStructure.js";
  const props = defineProps({
    modelValue: { type: Boolean, default: false },
@@ -158,64 +184,67 @@
  const materialTableLoading = ref(false);
  const materialSaving = ref(false);
  const materialTableData = ref([]);
  const selectedMaterialRows = ref([]);
  const purchaseLedgerDialogVisible = ref(false);
  const purchaseLedgerAddDialogRef = ref(null);
  const isSaveDisabled = computed(() => {
    if (materialTableLoading.value) return true;
    if (materialTableData.value.length === 0) return true;
    return !materialTableData.value.some(row => {
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»»ä½•用户输入内容
      const hasBatch = Array.isArray(row.batchNo) && row.batchNo.length > 0;
      const hasPickQty =
        row.pickQty !== null && row.pickQty !== undefined && row.pickQty !== 0;
      if (row.bom) {
        // å¯¹äºŽæ¥è‡ªBOM的行,输入框只有“批号”和“领用数量”
        return hasBatch || hasPickQty;
      } else {
        // å¯¹äºŽæ–°å¢žè¡Œï¼Œè¾“入框包括“工序”、“原料”、“需求数量”、“批号”和“领用数量”
        const hasOperation = !!row.operationName;
        const hasMaterial = !!row.materialName;
        const hasDemanded =
          row.demandedQuantity !== null &&
          row.demandedQuantity !== undefined &&
          row.demandedQuantity !== 0;
        return (
          hasBatch || hasPickQty || hasOperation || hasMaterial || hasDemanded
        );
      }
    });
    // åªæœ‰å­˜åœ¨æœ¬æ¬¡é¢†ç”¨æ•°é‡å¤§äºŽ0的行才允许保存
    return !materialTableData.value.some(item => Number(item.pickQty) > 0);
  });
  const processOptions = ref([]);
  const currentMaterialSelectRowIndex = ref(-1);
  let materialTempId = 0;
  const getDefaultBatchNo = batchNoList => {
    if (!Array.isArray(batchNoList) || batchNoList.length === 0) return [];
    return [batchNoList[0]];
  const toNumber = val => {
    const n = Number(val);
    return Number.isFinite(n) ? n : 0;
  };
  // ä¿ç•™6位小数,避免浮点误差
  const roundQuantity = val => Math.round(toNumber(val) * 1e6) / 1e6;
  // éœ€æ±‚数量分组键:与后端保持一致(工序ID优先,缺失时用工序名称)
  const buildDemandKey = (technologyOperationId, operationName, productModelId) =>
    `${technologyOperationId != null && technologyOperationId !== undefined ? "T" + technologyOperationId : "N" + (operationName || "")}#${productModelId ?? ""}`;
  const stripTrailingZeros = val => {
    if (val === null || val === undefined || val === "") return "0";
    const str = String(val);
    if (str.includes(".")) {
      return parseFloat(str).toString();
    }
    return str;
  };
  const createMaterialRow = (row = {}) => {
    const batchNoList = Array.isArray(row.batchNoList) ? row.batchNoList : [];
    const batchNo = row.batchNo
      ? typeof row.batchNo === "string"
        ? row.batchNo.split(",")
        : row.batchNo
      : getDefaultBatchNo(batchNoList);
    const pickedEntry = row.pickedEntry || null;
    const pickedQty = pickedEntry ? roundQuantity(pickedEntry.pickedQty) : 0;
    const returnedQty = pickedEntry ? roundQuantity(pickedEntry.returnedQty) : 0;
    const demandedQuantity = roundQuantity(row.demandedQuantity ?? 0);
    const remainingQty = Math.max(roundQuantity(demandedQuantity - pickedQty + returnedQty), 0);
    return {
      tempId: row.id || `temp_${++materialTempId}`,
      id: row.id,
      processId: row.processId || row.technologyOperationId,
      technologyOperationId: row.technologyOperationId || row.processId,
      operationName: row.operationName || "",
      tempId: `temp_${++materialTempId}`,
      id: null,
      bom: row.bom === true,
      materialModelId: row.materialModelId || row.productModelId,
      materialName: row.materialName || row.productName || "",
      materialModel: row.materialModel || row.model || "",
      demandedQuantity: Number(row.requiredQty ?? row.demandedQuantity ?? 0),
      technologyOperationId: row.technologyOperationId ?? null,
      operationName: row.operationName || "",
      materialModelId: row.materialModelId ?? null,
      materialName: row.materialName || "",
      materialModel: row.materialModel || "",
      unit: row.unit || "",
      pickQty: Number(row.pickQty ?? row.pickQuantity ?? row.requiredQty ?? row.demandedQuantity ?? 0),
      batchNo,
      demandedQuantity,
      pickedQty,
      returnedQty,
      remainingQty,
      pickQty: 0,
      stockQty: null,
      batchNo: [],
      batchNoList,
    };
  };
@@ -237,37 +266,115 @@
      if (!processMap.has(key)) {
        processMap.set(key, {
          id: processId,
          technologyOperationId: processId,
          name: operationName,
        });
      }
    });
    processOptions.value = Array.from(processMap.values());
  };
  const isDetail = ref(true);
  const loadMaterialData = async () => {
    if (!props.orderRow?.id) return;
    materialTableLoading.value = true;
    materialTableData.value = [];
    await getProcessOptions();
    try {
      const detailRes = await listMaterialPickingDetail(props.orderRow.id);
      const detailList = Array.isArray(detailRes?.data)
        ? detailRes.data
        : detailRes?.data?.records || [];
      if (detailList.length > 0) {
        isDetail.value = true;
        materialTableData.value = detailList.map(item => createMaterialRow(item));
        return;
      } else {
        isDetail.value = false;
        const bomRes = await listMaterialPickingBom(props.orderRow.id);
      await getProcessOptions();
      // åŒæ—¶èŽ·å–BOM需求数据与已领料明细,用于计算“已领数量/剩余可领”
      const [bomRes, detailRes] = await Promise.all([
        listMaterialPickingBom(props.orderRow.id),
        listMaterialPickingDetail(props.orderRow.id),
      ]);
        const bomList = Array.isArray(bomRes?.data)
          ? bomRes.data
          : bomRes?.data?.records || [];
        materialTableData.value = bomList.map(item => createMaterialRow(item));
        return;
      const detailList = Array.isArray(detailRes?.data)
        ? detailRes.data
        : detailRes?.data?.records || [];
      // æŒ‰â€œå·¥åº+原料”汇总历史领料:已领数量、退料数量
      const pickedMap = new Map();
      detailList.forEach(item => {
        if (!item) return;
        const key = buildDemandKey(
          item.technologyOperationId,
          item.operationName,
          item.productModelId
        );
        const entry = pickedMap.get(key) || {
          pickedQty: 0,
          returnedQty: 0,
          manualDemand: 0,
          technologyOperationId: item.technologyOperationId,
          operationName: item.operationName || "",
          materialModelId: item.productModelId,
          materialName: item.productName || "",
          materialModel: item.model || "",
          unit: item.unit || "",
          batchNoList: Array.isArray(item.batchNoList)
            ? item.batchNoList
            : [],
        };
        entry.pickedQty = roundQuantity(entry.pickedQty + toNumber(item.pickQuantity));
        entry.returnedQty = roundQuantity(entry.returnedQty + toNumber(item.returnQty));
        // æ‰‹å·¥æ–°å¢žè¡Œï¼ˆéžBOM)的需求数量以领料快照为准
        if (item.bom !== true && item.demandedQuantity != null) {
          entry.manualDemand = Math.max(
            toNumber(entry.manualDemand),
            toNumber(item.demandedQuantity)
          );
      }
        pickedMap.set(key, entry);
      });
      const rows = [];
      const bomKeySet = new Set();
      // BOM行:需求数量取BOM,已领数量取历史领料汇总
      bomList.forEach(item => {
        const materialModelId = item.productModelId ?? item.materialModelId;
        const key = buildDemandKey(
          item.technologyOperationId,
          item.operationName,
          materialModelId
        );
        bomKeySet.add(key);
        rows.push(
          createMaterialRow({
            bom: true,
            technologyOperationId: item.technologyOperationId,
            operationName: item.operationName || "",
            materialModelId,
            materialName: item.productName || item.materialName || "",
            materialModel: item.model || item.materialModel || "",
            unit: item.unit || "",
            demandedQuantity: item.demandedQuantity ?? item.requiredQty ?? 0,
            pickedEntry: pickedMap.get(key),
            batchNoList: item.batchNoList || [],
          })
        );
      });
      // ä¸åœ¨BOM中的历史领料(手工新增行),也展示出来支持继续领料
      pickedMap.forEach((entry, key) => {
        if (bomKeySet.has(key)) return;
        rows.push(
          createMaterialRow({
            bom: false,
            technologyOperationId: entry.technologyOperationId,
            operationName: entry.operationName,
            materialModelId: entry.materialModelId,
            materialName: entry.materialName,
            materialModel: entry.materialModel,
            unit: entry.unit,
            demandedQuantity: entry.manualDemand,
            pickedEntry: entry,
            batchNoList: entry.batchNoList,
          })
        );
      });
      // å…ˆæŸ¥å‡ºåº“存余量再入表,保证首次打开即展示(rows å°šæœªè½¬ä¸ºå“åº”式代理,
      // å…¥è¡¨åŽå†æ”¹åŽŸå§‹å¯¹è±¡ä¸ä¼šè§¦å‘è§†å›¾æ›´æ–°ï¼‰
      await loadStockQtyForRows(rows);
      materialTableData.value = rows;
    } finally {
      materialTableLoading.value = false;
    }
@@ -282,21 +389,74 @@
    }
  );
  const stripTrailingZeros = val => {
    const str = String(val);
    if (str.includes(".")) {
      return parseFloat(str).toString();
    }
    return str;
  };
  const handleClose = () => {
    materialTableData.value = [];
    selectedMaterialRows.value = [];
    currentMaterialSelectRowIndex.value = -1;
  };
  // æŒ‰åž‹å·æ±‡æ€»æ‰€æœ‰æ‰¹æ¬¡çš„原料可用库存余量
  const loadStockQtyForRows = async rows => {
    const modelIds = [
      ...new Set(rows.map(row => row.materialModelId).filter(Boolean)),
    ];
    const stockMap = new Map();
    await Promise.all(
      modelIds.map(async modelId => {
        try {
          const res = await getStockInventoryByModelId(modelId);
          const list = Array.isArray(res?.data)
            ? res.data
            : res?.data?.records || res?.data?.rows || [];
          const total = list.reduce(
            (sum, item) => sum + toNumber(item?.qualitity ?? item?.quantity),
            0
          );
          stockMap.set(modelId, roundQuantity(total));
        } catch (e) {
          stockMap.set(modelId, 0);
        }
      })
    );
    rows.forEach(row => {
      if (!row.materialModelId) {
        row.stockQty = null;
        return;
      }
      row.stockQty = stockMap.has(row.materialModelId)
        ? stockMap.get(row.materialModelId)
        : 0;
    });
  };
  const handleAddMaterialRow = () => {
    materialTableData.value.push(createMaterialRow());
  };
  const handleMaterialSelectionChange = selection => {
    selectedMaterialRows.value = selection;
  };
  const handleAddPurchase = () => {
    if (selectedMaterialRows.value.length === 0) {
      ElMessage.warning("请选择需要新增采购的原料行");
      return;
    }
    const rowsWithModel = selectedMaterialRows.value.filter(
      row => row.materialModelId
    );
    if (rowsWithModel.length === 0) {
      ElMessage.warning("所选行缺少原料型号,请先选择原料后再新增采购");
      return;
    }
    const productRows = rowsWithModel.map(row => ({
      productModelId: row.materialModelId,
      productCategory: row.materialName,
      specificationModel: row.materialModel,
      unit: row.unit,
      isChecked: false,
    }));
    purchaseLedgerAddDialogRef.value?.open(productRows);
  };
  const handleDeleteMaterialRow = index => {
@@ -311,11 +471,12 @@
  };
  const handleRequiredQtyChange = (row, val) => {
    const required = Number(val ?? 0);
    const required = roundQuantity(val);
    row.demandedQuantity = required;
    if (!row.pickQty || Number(row.pickQty) === 0) {
      row.pickQty = required;
    }
    row.remainingQty = Math.max(
      roundQuantity(required - toNumber(row.pickedQty) + toNumber(row.returnedQty)),
      0
    );
  };
  const openMaterialProductSelect = row => {
@@ -326,8 +487,6 @@
  };
  const handleMaterialProductConfirm = products => {
    console.log(products, "products");
    if (!products || products.length === 0) return;
    const index = currentMaterialSelectRowIndex.value;
    if (index < 0 || !materialTableData.value[index]) return;
@@ -340,42 +499,32 @@
    row.materialModel = product.materialModel || product.model || "";
    row.unit = product.unit || product.measureUnit || "";
    row.batchNoList = Array.isArray(product.batchNoList) ? product.batchNoList : [];
    row.batchNo = getDefaultBatchNo(row.batchNoList);
    row.batchNo = [];
    currentMaterialSelectRowIndex.value = -1;
    materialProductDialogVisible.value = false;
    loadStockQtyForRows([row]);
  };
  const validateMaterialRows = () => {
    if (materialTableData.value.length === 0) {
      return { valid: false, message: "请先新增领料数据" };
    }
    const invalidNewRow = materialTableData.value.find(
      item => item.bom !== true && (!item.operationName || !item.materialName)
    const pickRows = materialTableData.value.filter(
      item => toNumber(item.pickQty) > 0
    );
    if (invalidNewRow) {
      return { valid: false, message: "新增行的工序名称和原料名称为必填项" };
    if (pickRows.length === 0) {
      return { valid: false, message: "请至少填写一行大于0的领用数量" };
    }
    const invalidRow = materialTableData.value.find(
      item =>
        !item.operationName ||
        !item.materialName ||
        (Number(item.pickQty) > 0 &&
          (!item.batchNo || item.batchNo.length === 0)) ||
        (item.batchNo && item.batchNo.length > 0 && Number(item.pickQty) <= 0) ||
        item.demandedQuantity === null ||
        item.demandedQuantity === undefined ||
        item.pickQty === null ||
        item.pickQty === undefined
    );
    if (invalidRow) {
      if (
        invalidRow.batchNo &&
        invalidRow.batchNo.length > 0 &&
        Number(invalidRow.pickQty) <= 0
      ) {
        return { valid: false, message: "选择了批号时,领用数量必须大于零" };
    for (let i = 0; i < materialTableData.value.length; i++) {
      const item = materialTableData.value[i];
      const rowNo = i + 1;
      if (toNumber(item.pickQty) <= 0) continue;
      if (item.bom !== true && (!item.operationName || !item.materialName)) {
        return { valid: false, message: `第${rowNo}行的工序名称和原料名称为必填项` };
      }
      return { valid: false, message: "请完善工序、原料、批号和数量后再保存" };
      if (!Array.isArray(item.batchNo) || item.batchNo.length === 0) {
        return { valid: false, message: `第${rowNo}行请选择批号` };
      }
    }
    return { valid: true, message: "" };
  };
@@ -389,47 +538,26 @@
    }
    materialSaving.value = true;
    try {
      if (isDetail.value) {
        await updateMaterialPickingLedger({
          productionOrderId: props.orderRow.id,
          productionOrderPickDto: materialTableData.value.map(item => ({
            id: item.id,
            // processId: item.operationName,
            technologyOperationId: item.technologyOperationId,
            operationName: item.operationName,
            bom: item.bom === true,
            productModelId: item.materialModelId,
            // materialName: item.materialName,
            // materialModel: item.materialModel,
            demandedQuantity: item.demandedQuantity,
            unit: item.unit,
            pickQuantity: item.pickQty,
            batchNo: Array.isArray(item.batchNo)
              ? item.batchNo.join(",")
              : item.batchNo,
          })),
        });
      } else {
      // æ¯æ¬¡ä¿å­˜éƒ½æ˜¯ä¸€æ¬¡ç‹¬ç«‹çš„领料:只提交本次领用数量大于0的行,
      // å‡ºåº“台账按本次领料数量记账,历史领料记录保持不变。
      const pickRows = materialTableData.value.filter(
        item => toNumber(item.pickQty) > 0
      );
        await saveMaterialPickingLedger({
          productionOrderId: props.orderRow.id,
          productionOrderPickDto: materialTableData.value.map(item => ({
            id: item.id,
            // processId: item.operationName,
        productionOrderPickDto: pickRows.map(item => ({
            technologyOperationId: item.technologyOperationId,
            operationName: item.operationName,
            bom: item.bom === true,
            productModelId: item.materialModelId,
            // materialName: item.materialName,
            // materialModel: item.materialModel,
            demandedQuantity: item.demandedQuantity,
            unit: item.unit,
            pickQuantity: item.pickQty,
          pickQuantity: toNumber(item.pickQty),
            batchNo: Array.isArray(item.batchNo)
              ? item.batchNo.join(",")
              : item.batchNo,
          })),
        });
      }
      ElMessage({ message: "领料成功", type: "success" });
      emit("saved");
@@ -445,4 +573,9 @@
    margin-bottom: 12px;
    text-align: right;
  }
  .remaining-empty {
    color: #f56c6c;
    font-weight: 600;
  }
</style>
src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue
@@ -31,7 +31,7 @@
          </div>
          <div class="info-item"
               style="width: 25%;">
            <span class="label">需求日期:</span>
            <span class="label">交货日期:</span>
            <span class="value">{{ formatDate(orderRow?.planCompleteTime) }}</span>
          </div>
        </div>
src/views/productionManagement/productionOrder/components/PurchaseLedgerAddDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,856 @@
<template>
  <div>
    <FormDialog v-model="dialogVisible"
                title="新增采购台账页面"
                :width="'70%'"
                operation-type="add"
                @close="closeDia"
                @confirm="submitForm"
                @cancel="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="采购合同号:" prop="purchaseContractNumber">
              <el-input v-model="form.purchaseContractNumber" placeholder="请输入" clearable />
            </el-form-item>
          </el-col>
          <el-col v-if="showSalesContractBinding" :span="12">
            <el-form-item label="销售合同号:" prop="salesLedgerId">
              <el-select v-model="form.salesLedgerId"
                         placeholder="请选择"
                         filterable
                         clearable
                         @change="salesLedgerChange">
                <el-option v-for="item in salesContractList"
                           :key="item.id"
                           :label="item.salesContractNo"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="供应商名称:" prop="supplierId">
              <el-select v-model="form.supplierId" placeholder="请选择" filterable clearable>
                <el-option v-for="item in supplierList"
                           :key="item.id"
                           :label="item.supplierName"
                           :value="item.id">
                  {{ item.supplierName }}
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="项目名称" prop="projectName">
              <el-input v-model="form.projectName" placeholder="请输入" clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="付款方式">
              <el-input v-model="form.paymentMethod" placeholder="请输入" clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="签订日期:" prop="executionDate">
              <el-date-picker v-model="form.executionDate"
                              style="width: 100%"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="录入人:" prop="recorderId">
              <el-select v-model="form.recorderId"
                         placeholder="请选择"
                         clearable
                         filterable
                         disabled>
                <el-option v-for="item in userList"
                           :key="item.userId"
                           :label="item.nickName"
                           :value="item.userId" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="录入日期:" prop="entryDate">
              <el-date-picker v-model="form.entryDate"
                              style="width: 100%"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              type="date"
                              placeholder="请选择"
                              clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-form-item label="产品信息:">
            <el-button type="primary" @click="openProductForm('add')">添加</el-button>
            <el-button plain type="danger" @click="deleteProduct">删除</el-button>
          </el-form-item>
          <div class="select-button-group">
            <el-select v-model="templateName"
                       filterable
                       allow-create
                       :reserve-keyword="true"
                       :default-first-option="false"
                       clearable
                       :input-value="filterInputValue"
                       @filter-change="onTemplateFilterChange"
                       @change="onTemplateChange"
                       @focus="getTemplateList"
                       placeholder="请选择模版或者输入新的模版名称后选择"
                       class="no-arrow-select template-select">
              <el-option v-for="item in templateList"
                         :key="item.id || item.value"
                         :label="item.templateName"
                         :value="item.templateName">
                <div class="template-option">
                  <span>{{ item.templateName }}</span>
                  <el-icon v-if="item.id"
                           class="delete-icon"
                           @click.stop="handleDeleteTemplate(item)">
                    <Delete />
                  </el-icon>
                </div>
              </el-option>
            </el-select>
            <el-button size="small"
                       class="template-save-button"
                       :disabled="!templateName || templateName.trim() === '' || (!currentTemplateId && isTemplateNameDuplicate)"
                       @click="handleButtonClick">
              ä¿å­˜
            </el-button>
          </div>
        </el-row>
        <el-table :data="productData"
                  border
                  show-summary
                  :summary-method="summarizeProTable"
                  @selection-change="productSelected">
          <el-table-column align="center" type="selection" width="55" />
          <el-table-column align="center" label="序号" type="index" width="60" />
          <el-table-column label="产品大类" prop="productCategory" />
          <el-table-column label="规格型号" prop="specificationModel" />
          <el-table-column label="单位" prop="unit" width="70" />
          <el-table-column label="数量" prop="quantity" width="70" />
          <el-table-column label="税率(%)" prop="taxRate" width="80" />
          <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" width="150" />
          <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" width="150" />
          <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" width="150" />
          <el-table-column label="是否质检" prop="isChecked" width="150">
            <template #default="{ row }">
              <el-tag :type="row.isChecked ? 'success' : 'info'">
                {{ row.isChecked ? '是' : '否' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column fixed="right" label="操作" min-width="60" align="center">
            <template #default="{ row, $index }">
              <el-button link type="primary" @click="openProductForm('edit', row, $index)">编辑</el-button>
            </template>
          </el-table-column>
        </el-table>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注·:" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入" clearable type="textarea" :rows="2" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:" prop="purchaseLedgerFiles">
              <FileUpload v-model:file-list="fileList" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </FormDialog>
    <FormDialog v-model="productFormVisible"
                :title="productOperationType === 'add' ? '新增产品' : '编辑产品'"
                :width="'40%'"
                :operation-type="productOperationType"
                @close="closeProductDia"
                @confirm="submitProduct"
                @cancel="closeProductDia">
      <el-form :model="productForm"
               label-width="140px"
               label-position="top"
               :rules="productRules"
               ref="productFormRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="产品大类:" prop="productCategory">
              <el-input v-model="productForm.productCategory"
                        placeholder="点击选择"
                        readonly
                        style="cursor:pointer"
                        @click="showProductSelectDialog = true" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="productModelId">
              <el-input v-model="productForm.specificationModel"
                        placeholder="点击选择"
                        readonly
                        style="cursor:pointer"
                        @click="showProductSelectDialog = true" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="单位:" prop="unit">
              <el-input v-model="productForm.unit" placeholder="请输入" clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="税率(%):" prop="taxRate">
              <el-select v-model="productForm.taxRate" placeholder="请选择" clearable @change="mathNum">
                <el-option v-for="dict in tax_rate"
                           :key="dict.value"
                           :label="dict.label"
                           :value="dict.value" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="含税单价(元):" prop="taxInclusiveUnitPrice">
              <el-input-number v-model="productForm.taxInclusiveUnitPrice"
                               :precision="6"
                               :step="0.000001"
                               :min="0"
                               clearable
                               style="width: 100%"
                               @change="mathNum" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number v-model="productForm.quantity"
                               :step="0.1"
                               clearable
                               :precision="2"
                               :min="0"
                               style="width: 100%"
                               placeholder="请输入"
                               @change="mathNum" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="含税总价(元):" prop="taxInclusiveTotalPrice">
              <el-input-number v-model="productForm.taxInclusiveTotalPrice"
                               :precision="6"
                               :step="0.000001"
                               :min="0"
                               clearable
                               style="width: 100%"
                               @change="reverseMathNum('taxInclusiveTotalPrice')" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不含税总价(元):" prop="taxExclusiveTotalPrice">
              <el-input-number v-model="productForm.taxExclusiveTotalPrice"
                               :precision="6"
                               :step="0.000001"
                               :min="0"
                               clearable
                               style="width: 100%"
                               @change="reverseMathNum('taxExclusiveTotalPrice')" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="发票类型:" prop="invoiceType">
              <el-select v-model="productForm.invoiceType" placeholder="请选择" clearable>
                <el-option label="增普票" value="增普票" />
                <el-option label="增专票" value="增专票" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否质检:" prop="isChecked">
              <el-radio-group v-model="productForm.isChecked">
                <el-radio label="是" :value="true" />
                <el-radio label="否" :value="false" />
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </FormDialog>
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
  </div>
</template>
<script setup>
  import { computed, getCurrentInstance, nextTick, reactive, ref, toRefs } from "vue";
  import { Delete } from "@element-plus/icons-vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import useUserStore from "@/store/modules/user";
  import { userListNoPage } from "@/api/system/user.js";
  import { getProductInfoByContractNo } from "@/api/salesManagement/salesLedger.js";
  import {
    addOrEditPurchase,
    addPurchaseTemplate,
    createPurchaseNo,
    delPurchaseTemplate,
    getOptions,
    getPurchaseTemplateList,
    getSalesNo,
    updatePurchaseTemplate,
  } from "@/api/procurementManagement/procurementLedger.js";
  import { tableAmountFormatter, formatDecimal, buildAmountSummaryFormat } from "@/utils/numberFormat";
  const props = defineProps({
    modelValue: { type: Boolean, default: false },
    initialProducts: { type: Array, default: () => [] },
  });
  const emit = defineEmits(["update:modelValue", "saved"]);
  const dialogVisible = computed({
    get: () => props.modelValue,
    set: val => emit("update:modelValue", val),
  });
  const { proxy } = getCurrentInstance();
  const { tax_rate } = proxy.useDict("tax_rate");
  const userStore = useUserStore();
  const showSalesContractBinding = false;
  const productData = ref([]);
  const productSelectedRows = ref([]);
  const userList = ref([]);
  const salesContractList = ref([]);
  const supplierList = ref([]);
  const fileList = ref([]);
  const productFormVisible = ref(false);
  const showProductSelectDialog = ref(false);
  const productOperationType = ref("");
  const productOperationIndex = ref(-1);
  const templateName = ref("");
  const filterInputValue = ref("");
  const templateList = ref([]);
  const isTemplateNameDuplicate = ref(false);
  const currentTemplateId = ref(null);
  let duplicateCheckTimer = null;
  const data = reactive({
    form: {
      purchaseContractNumber: "",
      salesLedgerId: "",
      projectName: "",
      recorderId: "",
      entryDate: "",
      productData: [],
      supplierName: "",
      supplierId: "",
      paymentMethod: "",
      executionDate: "",
      isChecked: false,
    },
    rules: {
      purchaseContractNumber: [{ required: true, message: "请输入", trigger: "blur" }],
      projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
      supplierId: [{ required: true, message: "请输入", trigger: "blur" }],
      entryDate: [{ required: true, message: "请选择", trigger: "change" }],
      executionDate: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { form, rules } = toRefs(data);
  const productFormData = reactive({
    productForm: {
      productId: "",
      productCategory: "",
      productModelId: "",
      specificationModel: "",
      unit: "",
      quantity: "",
      taxInclusiveUnitPrice: "",
      taxRate: "",
      taxInclusiveTotalPrice: "",
      taxExclusiveTotalPrice: "",
      invoiceType: "",
      warnNum: "",
      isChecked: false,
    },
    productRules: {
      productCategory: [{ required: true, message: "请选择产品大类", trigger: "change" }],
      productModelId: [{ required: true, message: "请选择规格型号", trigger: "change" }],
      unit: [{ required: true, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      taxInclusiveUnitPrice: [{ required: true, message: "请输入", trigger: "blur" }],
      taxRate: [{ required: true, message: "请选择", trigger: "change" }],
      taxInclusiveTotalPrice: [{ required: true, message: "请输入", trigger: "blur" }],
      taxExclusiveTotalPrice: [{ required: true, message: "请输入", trigger: "blur" }],
      invoiceType: [{ required: true, message: "请选择", trigger: "change" }],
      isChecked: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { productForm, productRules } = toRefs(productFormData);
  const formattedNumber = tableAmountFormatter;
  const getCurrentDate = () => {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, "0");
    const day = String(today.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
  };
  const normalizeProductRow = row => ({
    productId: row.productId || "",
    productCategory: row.productCategory || row.materialName || row.productName || "",
    productModelId: row.productModelId || row.materialModelId || row.modelId || "",
    specificationModel: row.specificationModel || row.materialModel || row.model || "",
    unit: row.unit || "",
    quantity: row.quantity || "",
    taxInclusiveUnitPrice: row.taxInclusiveUnitPrice || "",
    taxRate: row.taxRate || "",
    taxInclusiveTotalPrice: row.taxInclusiveTotalPrice || "",
    taxExclusiveTotalPrice: row.taxExclusiveTotalPrice || "",
    invoiceType: row.invoiceType || "",
    warnNum: row.warnNum || "",
    isChecked: row.isChecked ?? false,
  });
  const resetDialogState = () => {
    form.value = {
      purchaseContractNumber: "",
      salesLedgerId: "",
      projectName: "",
      recorderId: "",
      entryDate: "",
      productData: [],
      supplierName: "",
      supplierId: "",
      paymentMethod: "",
      executionDate: "",
      isChecked: false,
    };
    productData.value = [];
    productSelectedRows.value = [];
    fileList.value = [];
    templateName.value = "";
    filterInputValue.value = "";
    isTemplateNameDuplicate.value = false;
    currentTemplateId.value = null;
  };
  const open = async products => {
    resetDialogState();
    await Promise.all([
      getTemplateList(),
      userListNoPage().then(res => {
        userList.value = res.data || [];
      }),
      getSalesNo().then(res => {
        salesContractList.value = res || [];
      }),
      getOptions().then(res => {
        supplierList.value = (res.data || []).filter(item => item.isWhite === 0);
      }),
    ]);
    form.value.entryDate = getCurrentDate();
    form.value.recorderId = userStore.id;
    try {
      const purchaseNoRes = await createPurchaseNo();
      if (purchaseNoRes?.data) {
        form.value.purchaseContractNumber = purchaseNoRes.data;
      }
    } catch (error) {
      console.error("生成采购合同号失败:", error);
      proxy.$modal.msgWarning("生成采购合同号失败");
    }
    const sourceProducts = Array.isArray(products) ? products : props.initialProducts;
    productData.value = sourceProducts.map(normalizeProductRow);
    dialogVisible.value = true;
    await nextTick();
    if (productData.value.length > 0) {
      ElMessage.warning("已按所选领料行生成产品信息,请编辑补全数量、税率、单价、发票类型等必填项后再提交采购台账");
    }
  };
  defineExpose({ open });
  const checkTemplateNameDuplicate = name => {
    if (!name || name.trim() === "") {
      isTemplateNameDuplicate.value = false;
      return false;
    }
    const isDuplicate = templateList.value.some(item => item.templateName === name.trim());
    isTemplateNameDuplicate.value = isDuplicate;
    return isDuplicate;
  };
  const onTemplateFilterChange = val => {
    filterInputValue.value = val ?? "";
    if (duplicateCheckTimer) {
      clearTimeout(duplicateCheckTimer);
    }
    if (val && val.trim()) {
      duplicateCheckTimer = setTimeout(() => {
        if (checkTemplateNameDuplicate(val)) {
          ElMessage.warning("模板名称已存在,请更换模板名称");
        }
      }, 300);
    } else {
      isTemplateNameDuplicate.value = false;
    }
  };
  const onTemplateChange = async val => {
    if (typeof val === "string") {
      filterInputValue.value = val;
      checkTemplateNameDuplicate(val);
    }
    const matchedTemplate = templateList.value.find(item => item.templateName === val);
    if (matchedTemplate?.id) {
      currentTemplateId.value = matchedTemplate.id;
      isTemplateNameDuplicate.value = false;
      form.value.supplierId = matchedTemplate.supplierId || form.value.supplierId;
      form.value.supplierName = matchedTemplate.supplierName || form.value.supplierName;
      form.value.projectName = matchedTemplate.projectName || form.value.projectName;
      form.value.paymentMethod = matchedTemplate.paymentMethod || form.value.paymentMethod;
      productData.value = matchedTemplate.productList || matchedTemplate.productData || [];
    } else {
      currentTemplateId.value = null;
    }
  };
  const getTemplateList = async () => {
    const res = await getPurchaseTemplateList();
    if (res && res.code === 200 && Array.isArray(res.data)) {
      templateList.value = res.data;
    }
  };
  const handleButtonClick = async () => {
    if (!templateName.value || templateName.value.trim() === "") {
      ElMessage.warning("请输入模板名称");
      return;
    }
    if (!currentTemplateId.value && checkTemplateNameDuplicate(templateName.value)) {
      ElMessage.warning("模板名称已存在,请更换模板名称");
      return;
    }
    if (!form.value.supplierId) {
      ElMessage.warning("请先选择供应商");
      return;
    }
    if (!productData.value || productData.value.length === 0) {
      ElMessage.warning("请先添加产品信息");
      return;
    }
    const params = {
      productData: proxy.HaveJson(productData.value),
      supplierId: form.value.supplierId,
      paymentMethod: form.value.paymentMethod,
      recorderId: form.value.recorderId,
      projectName: form.value.projectName,
      templateName: templateName.value.trim(),
    };
    const res = currentTemplateId.value
      ? await updatePurchaseTemplate({ id: currentTemplateId.value, ...params })
      : await addPurchaseTemplate(params);
    if (res && res.code === 200) {
      ElMessage.success(currentTemplateId.value ? "模板更新成功" : "模板保存成功");
      await getTemplateList();
      templateName.value = "";
      filterInputValue.value = "";
      isTemplateNameDuplicate.value = false;
      currentTemplateId.value = null;
    } else {
      ElMessage.error(res?.msg || "模板保存失败");
    }
  };
  const productSelected = selectedRows => {
    productSelectedRows.value = selectedRows;
  };
  const openProductForm = (type, row, index) => {
    productOperationType.value = type;
    productOperationIndex.value = index ?? -1;
    proxy.resetForm("productFormRef");
    productForm.value = type === "edit" ? { ...row } : { isChecked: false };
    productFormVisible.value = true;
  };
  const handleProductSelect = products => {
    if (!products || !products.length) return;
    const p = products[0];
    productForm.value.productId = p.productId || "";
    productForm.value.productCategory = p.productName || p.topProductName || "";
    productForm.value.productModelId = p.id;
    productForm.value.specificationModel = p.model || "";
    productForm.value.unit = p.unit || "";
    showProductSelectDialog.value = false;
  };
  const closeProductDia = () => {
    proxy.resetForm("productFormRef");
    productFormVisible.value = false;
  };
  const submitProduct = () => {
    proxy.$refs["productFormRef"].validate(valid => {
      if (!valid) return;
      if (productOperationType.value === "add") {
        productData.value.push({ ...productForm.value });
      } else if (productOperationIndex.value > -1) {
        productData.value[productOperationIndex.value] = { ...productForm.value };
      }
      closeProductDia();
    });
  };
  const deleteProduct = () => {
    if (productSelectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    productData.value = productData.value.filter(item => !productSelectedRows.value.includes(item));
    productSelectedRows.value = [];
  };
  const summarizeProTable = param => proxy.summarizeTable(
    param,
    ["taxInclusiveUnitPrice", "taxInclusiveTotalPrice", "taxExclusiveTotalPrice"],
    buildAmountSummaryFormat(["taxInclusiveUnitPrice", "taxInclusiveTotalPrice", "taxExclusiveTotalPrice"])
  );
  const mathNum = () => {
    if (!productForm.value.taxRate) {
      proxy.$modal.msgWarning("请先选择税率");
      return;
    }
    if (!productForm.value.taxInclusiveUnitPrice || !productForm.value.quantity) return;
    productForm.value.taxInclusiveTotalPrice = proxy.calculateTaxIncludeTotalPrice(
      productForm.value.taxInclusiveUnitPrice,
      productForm.value.quantity
    );
    productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
      productForm.value.taxInclusiveTotalPrice,
      productForm.value.taxRate
    );
  };
  const reverseMathNum = field => {
    if (!productForm.value.taxRate) {
      proxy.$modal.msgWarning("请先选择税率");
      return;
    }
    const taxRate = Number(productForm.value.taxRate);
    if (!taxRate) return;
    if (field === "taxInclusiveTotalPrice") {
      if (productForm.value.quantity) {
        productForm.value.taxInclusiveUnitPrice = formatDecimal(
          Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity)
        );
      } else if (productForm.value.taxInclusiveUnitPrice) {
        productForm.value.quantity = (
          Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice)
        ).toFixed(2);
      }
      productForm.value.taxExclusiveTotalPrice = formatDecimal(
        Number(productForm.value.taxInclusiveTotalPrice) / (1 + taxRate / 100)
      );
    } else if (field === "taxExclusiveTotalPrice") {
      productForm.value.taxInclusiveTotalPrice = formatDecimal(
        Number(productForm.value.taxExclusiveTotalPrice) * (1 + taxRate / 100)
      );
      if (productForm.value.quantity) {
        productForm.value.taxInclusiveUnitPrice = formatDecimal(
          Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity)
        );
      } else if (productForm.value.taxInclusiveUnitPrice) {
        productForm.value.quantity = (
          Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice)
        ).toFixed(2);
      }
    }
  };
  const salesLedgerChange = async () => {
    await querygProductInfoByContractNo();
  };
  const querygProductInfoByContractNo = async () => {
    const { code, data } = await getProductInfoByContractNo({
      contractNo: form.value.salesLedgerId,
    });
    if (code === 200) {
      productData.value = data;
    }
  };
  const validateProductRows = () => {
    if (productData.value.length === 0) {
      return "请添加产品信息";
    }
    const requiredFields = [
      ["productCategory", "产品大类"],
      ["productModelId", "规格型号"],
      ["unit", "单位"],
      ["quantity", "数量"],
      ["taxRate", "税率"],
      ["taxInclusiveUnitPrice", "含税单价"],
      ["taxInclusiveTotalPrice", "含税总价"],
      ["taxExclusiveTotalPrice", "不含税总价"],
      ["invoiceType", "发票类型"],
    ];
    for (let i = 0; i < productData.value.length; i++) {
      const row = productData.value[i];
      const missing = requiredFields.find(([field]) => row[field] === "" || row[field] === null || row[field] === undefined);
      if (missing) {
        return `第${i + 1}行产品信息缺少${missing[1]},请编辑完再提交`;
      }
    }
    return "";
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (!valid) return;
      const productMessage = validateProductRows();
      if (productMessage) {
        proxy.$modal.msgWarning(productMessage);
        return;
      }
      const processedProductData = productData.value.map(product => {
        const { id, ...rest } = product;
        return rest;
      });
      const submitData = {
        ...form.value,
        productData: proxy.HaveJson(processedProductData),
        storageBlobDTOS: fileList.value,
        type: 2,
      };
      if (!submitData.salesLedgerId) {
        submitData.salesContractNo = "";
      }
      delete submitData.id;
      addOrEditPurchase(submitData).then(() => {
        proxy.$modal.msgSuccess("提交成功");
        closeDia();
        emit("saved");
      });
    });
  };
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogVisible.value = false;
  };
  const handleDeleteTemplate = async item => {
    if (!item.id) {
      proxy.$modal.msgWarning("无法删除该模板");
      return;
    }
    try {
      await ElMessageBox.confirm(`确定要删除模板"${item.templateName}"吗?`, "删除确认", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });
      const res = await delPurchaseTemplate([item.id]);
      if (res && res.code === 200) {
        ElMessage.success("删除成功");
        if (templateName.value === item.templateName) {
          templateName.value = "";
          filterInputValue.value = "";
        }
        await getTemplateList();
      } else {
        ElMessage.error(res?.msg || "删除失败");
      }
    } catch (error) {
      if (error !== "cancel") {
        ElMessage.error("删除失败,请稍后重试");
      }
    }
  };
</script>
<style scoped lang="scss">
  .el-row {
    justify-content: space-between;
    align-items: center;
  }
  .select-button-group {
    display: flex;
    align-items: center;
    width: 500px;
    margin: 20px 0;
  }
  .template-select {
    width: 500px;
  }
  .template-save-button {
    height: 32px;
    margin-left: 8px;
  }
  .template-option {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .no-arrow-select {
    --el-select-suffix-icon-color: transparent;
  }
  .delete-icon {
    cursor: pointer;
    color: #f56c6c;
    font-size: 14px;
    margin-left: 8px;
    transition: all 0.3s;
    &:hover {
      color: #f56c6c !important;
      transform: scale(1.2);
    }
  }
</style>
src/views/productionManagement/productionOrder/index.vue
@@ -11,6 +11,14 @@
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="销售单号:">
          <el-input v-model="searchForm.salesContractNo"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="产品名称:">
          <el-input v-model="searchForm.productName"
                    placeholder="请输入"
@@ -183,7 +191,7 @@
                  <div class="info-value">{{ item.unit || '-' }}</div>
                </div>
                <div class="info-item">
                  <div class="info-label">需求日期</div>
                  <div class="info-label">交货日期</div>
                  <div class="info-value">{{ item.requiredDate ? dayjs(item.requiredDate).format('YYYY-MM-DD') : '-' }}</div>
                </div>
              </div>
@@ -194,6 +202,7 @@
    </el-dialog>
    <MaterialLedgerDialog v-model="materialDialogVisible"
                          :order-row="currentMaterialOrder"
                          @saved="getList"
                           />
    <MaterialDetailDialog v-model="materialDetailDialogVisible"
                          :order-row="currentMaterialDetailOrder"
@@ -277,6 +286,11 @@
      prop: "npsNo",
      width: "150px",
    },
    {
      label: "销售单号",
      prop: "salesContractNo",
      width: "150px",
    },
    // 1.待开始、2.进行中、3.已完成、4.已取消、5.已结束
    {
      label: "状态",
src/views/productionManagement/productionProcess/ProcessSelectDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
<template>
  <el-dialog v-model="visible" title="选择工序" width="800px" destroy-on-close :close-on-click-modal="false">
    <el-form :inline="true" :model="query" class="mb-2">
      <el-form-item label="工序名称">
        <el-input v-model="query.name" placeholder="输入工序名称" clearable @keyup.enter="onSearch" style="width: 180px" />
      </el-form-item>
      <el-form-item label="工序编码">
        <el-input v-model="query.no" placeholder="输入工序编码" clearable @keyup.enter="onSearch" style="width: 180px" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSearch">搜索</el-button>
        <el-button @click="onReset">重置</el-button>
      </el-form-item>
    </el-form>
    <el-table ref="tableRef" v-loading="loading" :data="tableData" height="380" highlight-current-row row-key="id"
      @current-change="handleCurrentChange" @row-dblclick="onConfirm">
      <el-table-column type="index" label="序号" width="60" />
      <el-table-column prop="no" label="工序编码" min-width="130" />
      <el-table-column prop="name" label="工序名称" min-width="150" />
      <el-table-column prop="isQuality" label="是否质检" min-width="90">
        <template #default="{ row }">
          <el-tag size="small" :type="row.isQuality ? 'warning' : 'info'">{{ row.isQuality ? '是' : '否' }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="isProduction" label="是否生产" min-width="90">
        <template #default="{ row }">
          <el-tag size="small" :type="row.isProduction ? 'success' : 'info'">{{ row.isProduction ? '是' : '否' }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
    </el-table>
    <div class="mt-3 flex justify-end">
      <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
        v-model:page-size="page.pageSize" v-model:current-page="page.pageNum" :page-sizes="[10, 20, 50, 100]"
        @size-change="onPageChange" @current-change="onPageChange" />
    </div>
    <template #footer>
      <el-button type="primary" :disabled="!selectedRow" @click="onConfirm">确定</el-button>
      <el-button @click="close()">取消</el-button>
    </template>
  </el-dialog>
</template>
<script setup lang="ts">
  import { computed, reactive, ref, watch } from "vue";
  import { ElMessage } from "element-plus";
  import { listPage } from "@/api/productionManagement/productionProcess";
  export type ProcessRow = {
    id: number;
    no?: string;
    name: string;
    isQuality?: boolean;
    isProduction?: boolean;
    remark?: string;
  };
  const props = defineProps<{
    modelValue: boolean;
  }>();
  const emit = defineEmits(["update:modelValue", "confirm"]);
  const visible = computed({
    get: () => props.modelValue,
    set: v => emit("update:modelValue", v),
  });
  const query = reactive({
    name: "",
    no: "",
  });
  const page = reactive({
    pageNum: 1,
    pageSize: 10,
  });
  const loading = ref(false);
  const tableData = ref<ProcessRow[]>([]);
  const total = ref(0);
  const selectedRow = ref<ProcessRow | null>(null);
  const tableRef = ref();
  function close() {
    visible.value = false;
  }
  function handleCurrentChange(row: ProcessRow) {
    selectedRow.value = row;
  }
  function onSearch() {
    page.pageNum = 1;
    loadData();
  }
  function onReset() {
    query.name = "";
    query.no = "";
    onSearch();
  }
  function onPageChange() {
    loadData();
  }
  function onConfirm() {
    if (!selectedRow.value) {
      ElMessage.warning("请选择一个工序");
      return;
    }
    emit("confirm", selectedRow.value);
    close();
  }
  async function loadData() {
    loading.value = true;
    try {
      selectedRow.value = null;
      if (tableRef.value) {
        tableRef.value.setCurrentRow(null);
      }
      const { data } = await listPage({
        current: page.pageNum,
        size: page.pageSize,
        name: query.name.trim(),
        no: query.no.trim(),
      });
      const records = data?.records ?? (Array.isArray(data) ? data : []);
      tableData.value = Array.isArray(records) ? records : [];
      total.value = Number(data?.total ?? tableData.value.length);
    } catch (e) {
      console.error("加载工序列表失败", e);
      tableData.value = [];
      total.value = 0;
    } finally {
      loading.value = false;
    }
  }
  // ç›‘听弹窗打开,重置查询条件和页码(immediate:组件创建时 modelValue å·²ä¸º true,也立即加载)
  watch(
    () => props.modelValue,
    visible => {
      if (visible) {
        selectedRow.value = null;
        query.name = "";
        query.no = "";
        page.pageNum = 1;
        loadData();
      }
    },
    { immediate: true }
  );
</script>
src/views/productionManagement/productionTraceability/index.vue
@@ -157,7 +157,7 @@
            <el-table-column label="创建时间"
                             align="center">
              <template #default="{ row }">
                {{ parseTime(row.createTime) }}
                {{ parseTime(row.createTime, '{y}-{m}-{d}') }}
              </template>
            </el-table-column>
            <el-table-column label="工时(h)"
src/views/productionPlan/productionPlan/index.vue
@@ -21,7 +21,7 @@
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item label="需求日期范围:"
        <el-form-item label="交货日期范围:"
                      prop="dateRange">
          <el-date-picker v-model="searchForm.dateRange"
                          type="daterange"
@@ -212,13 +212,13 @@
          <el-input v-model="form.unit"
                    placeholder="请输入单位" />
        </el-form-item>
        <el-form-item label="需求日期"
        <el-form-item label="发货日期"
                      prop="requiredDate">
          <el-date-picker v-model="form.requiredDate"
                          type="date"
                          value-format="YYYY-MM-DD"
                          style="width: 100%"
                          placeholder="请选择需求日期" />
                          placeholder="请选择发货日期" />
        </el-form-item>
        <el-form-item label="承诺日期"
                      prop="promisedDeliveryDate">
@@ -371,7 +371,7 @@
      // formatData: (cell, row) => (cell ? `${cell}${row.unit || "方"}` : 0),
    },
    {
      label: "需求日期",
      label: "发货日期",
      prop: "requiredDate",
      width: "160px",
      className: "date-cell",
@@ -513,7 +513,7 @@
    ],
    qtyRequired: [{ required: true, message: "请输入数量", trigger: "blur" }],
    requiredDate: [
      { required: true, message: "请选择需求日期", trigger: "change" },
      { required: true, message: "请选择发货日期", trigger: "change" },
    ],
  });
@@ -530,8 +530,7 @@
    });
  };
  const onBlur = value => {
    // é™åˆ¶å››ä½å°æ•°
    mergeForm.totalAssignedQuantity = Number(value.toFixed(4));
    mergeForm.totalAssignedQuantity = Number(Number(value).toFixed(10));
  };
  const fetchProductOptions = () => {
@@ -656,9 +655,9 @@
        };
      }
      summary[category].totalAssignedQuantity += Number(
        (Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0)).toFixed(
          4
        )
        (
          Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0)
        ).toFixed(10)
      );
    });
@@ -739,13 +738,14 @@
    // è®¡ç®—总制造数量 (默认qtyRequired的和)
    const totalAssignedQuantity = selectedRows.value.reduce((sum, row) => {
      return sum + Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0);
    }, 0);
    sumAssignedQuantity.value = totalAssignedQuantity;
    console.log(totalAssignedQuantity);
    }, 0);  // é¿å…æµ®ç‚¹æ•°ç²¾åº¦é—®é¢˜ï¼ˆå¦‚ 37.800000000000004)
    const fixedTotal = Number(totalAssignedQuantity.toFixed(10));
    sumAssignedQuantity.value = fixedTotal;
    console.log(fixedTotal);
    // è®¾ç½®è¡¨å•数据
    mergeForm.productName = productName;
    mergeForm.model = firstRow.model || "";
    mergeForm.totalAssignedQuantity = totalAssignedQuantity;
    mergeForm.totalAssignedQuantity = fixedTotal;
    mergeForm.planCompleteTime = firstRow.requiredDate || "";
    mergeForm.productId = firstRow.productId || "";
    mergeForm.ids = selectedRows.value.map(row => row.id);
src/views/qualityManagement/finalInspection/index.vue
@@ -2,7 +2,16 @@
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">产品名称:</span>
        <span class="search_title">销售单号:</span>
        <el-input
            v-model="searchForm.salesContractNo"
            style="width: 240px"
            placeholder="请输入销售单号搜索"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <span style="margin-left: 10px" class="search_title">产品名称:</span>
        <el-input
            v-model="searchForm.productName"
            style="width: 240px"
@@ -80,6 +89,7 @@
const data = reactive({
  searchForm: {
    salesContractNo: "",
    productName: "",
    entryDate: undefined, // å½•入日期
    entryDateStart: undefined,
src/views/qualityManagement/nonconformingManagement/components/detailModal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
<template>
  <el-dialog
    v-model="visible"
    title="不合格处理详情"
    width="70%"
    @close="handleClose"
  >
    <el-descriptions border :column="2" class="detail-descriptions">
      <el-descriptions-item label="类别">
        {{ getInspectTypeName(form.inspectType) }}
      </el-descriptions-item>
      <el-descriptions-item label="产品名称">
        {{ form.productName }}
      </el-descriptions-item>
      <el-descriptions-item label="规格型号">
        {{ form.model }}
      </el-descriptions-item>
      <el-descriptions-item label="单位">
        {{ form.unit }}
      </el-descriptions-item>
      <el-descriptions-item label="数量">
        {{ form.quantity }}
      </el-descriptions-item>
      <el-descriptions-item label="检测日期">
        {{ form.checkTime }}
      </el-descriptions-item>
      <el-descriptions-item label="工序">
        {{ form.process }}
      </el-descriptions-item>
      <el-descriptions-item label="检验员">
        {{ form.checkName }}
      </el-descriptions-item>
      <el-descriptions-item label="检测单位">
        {{ form.checkCompany }}
      </el-descriptions-item>
      <el-descriptions-item label="不合格现象" :span="2">
        {{ form.defectivePhenomena }}
      </el-descriptions-item>
      <el-descriptions-item label="处理结果">
        {{ form.dealResult }}
      </el-descriptions-item>
      <el-descriptions-item label="处理人">
        {{ form.dealName }}
      </el-descriptions-item>
      <el-descriptions-item label="处理日期">
        {{ form.dealTime }}
      </el-descriptions-item>
    </el-descriptions>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">关闭</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref } from "vue";
import { getQualityUnqualifiedDetail } from "@/api/qualityManagement/nonconformingManagement.js";
const visible = ref(false);
const form = ref({});
const getInspectTypeName = (type) => {
  if (type === 0) return "原材料检验";
  if (type === 1) return "过程检验";
  if (type === 2) return "出厂检验";
  return "";
};
const openDialog = (row) => {
  visible.value = true;
  form.value = { ...row };
  if (row.id) {
    getQualityUnqualifiedDetail(row.id).then((res) => {
      if (res.data) {
        form.value = { ...res.data };
      }
    });
  }
};
const handleClose = () => {
  visible.value = false;
  form.value = {};
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
.detail-descriptions :deep(.el-descriptions__label) {
  width: 120px;
  justify-content: flex-end;
}
</style>
src/views/qualityManagement/nonconformingManagement/index.vue
@@ -55,6 +55,7 @@
    </div>
    <FormDia ref="formDia" @close="handleQuery"></FormDia>
    <InspectionFormDia ref="inspectionFormDia" @close="handleQuery"></InspectionFormDia>
    <DetailModal ref="detailModal"></DetailModal>
  </div>
</template>
@@ -65,6 +66,7 @@
import {ElMessageBox} from "element-plus";
import {qualityUnqualifiedDel, qualityUnqualifiedListPage} from "@/api/qualityManagement/nonconformingManagement.js";
import InspectionFormDia from "@/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue";
import DetailModal from "@/views/qualityManagement/nonconformingManagement/components/detailModal.vue";
import dayjs from "dayjs";
const data = reactive({
@@ -177,7 +179,7 @@
    label: "操作",
    align: "center",
    fixed: "right",
    width: 100,
    width: 150,
    operation: [
      {
        name: "处理",
@@ -186,6 +188,13 @@
          openInspectionForm("edit", row);
        },
        disabled: (row) => row.inspectState === 1,
      },
      {
        name: "详情",
        type: "text",
        clickFun: (row) => {
          detailModal.value?.openDialog(row);
        },
      },
    ],
  },
@@ -200,6 +209,7 @@
});
const formDia = ref()
const inspectionFormDia = ref()
const detailModal = ref()
const { proxy } = getCurrentInstance()
const changeDaterange = (value) => {
src/views/qualityManagement/processInspection/index.vue
@@ -2,7 +2,16 @@
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">工序:</span>
        <span class="search_title">生产工单号:</span>
        <el-input
            v-model="searchForm.workOrderNo"
            style="width: 240px"
            placeholder="请输入生产工单号搜索"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <span style="margin-left: 10px" class="search_title">工序:</span>
        <el-input
            v-model="searchForm.process"
            style="width: 240px"
@@ -79,6 +88,7 @@
const data = reactive({
  searchForm: {
    workOrderNo: "",
    process: "",
    entryDate: undefined, // å½•入日期
    entryDateStart: undefined,
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -2,7 +2,16 @@
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">供应商:</span>
        <span class="search_title">采购订单号:</span>
        <el-input
            v-model="searchForm.purchaseContractNo"
            style="width: 240px"
            placeholder="请输入采购订单号搜索"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <span style="margin-left: 10px" class="search_title">供应商:</span>
        <el-input
            v-model="searchForm.supplier"
            style="width: 240px"
@@ -81,6 +90,7 @@
const data = reactive({
  searchForm: {
    purchaseContractNo: "",
    supplier: "",
    entryDate: undefined, // å½•入日期
    entryDateStart: undefined,
src/views/salesManagement/salesLedger/index.vue
@@ -24,6 +24,18 @@
                    prefix-icon="Search"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="发货状态:">
          <el-select v-model="searchForm.shippingStatus"
                     placeholder="请选择"
                     clearable
                     style="width: 160px"
                     @change="handleQuery">
            <el-option label="未发货"
                       value="未发货" />
            <el-option label="已发货"
                       value="已发货" />
          </el-select>
        </el-form-item>
        <el-form-item label="录入日期:">
          <el-date-picker v-model="searchForm.entryDate"
                          value-format="YYYY-MM-DD"
@@ -910,7 +922,7 @@
    <!-- å‘货弹框 -->
    <el-dialog v-model="deliveryFormVisible"
               title="发货信息"
               width="40%"
               width="960px"
               @close="closeDeliveryDia">
      <el-form :model="deliveryForm"
               label-width="120px"
@@ -935,6 +947,12 @@
          <el-col :span="24">
            <el-form-item label="待发货数量:">
              <el-input :model-value="currentDeliveryRow?.noQuantity"
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="已选择发货数量:">
              <el-input :model-value="selectedDeliveryQuantity"
                        disabled />
            </el-form-item>
          </el-col>
@@ -990,6 +1008,13 @@
                <el-table-column label="批号"
                                 prop="batchNo"
                                 min-width="180" />
                <el-table-column label="销售订单号"
                                 min-width="180"
                                 show-overflow-tooltip>
                  <template #default="scope">
                    {{ formatDeliveryBatchSalesOrderNo(scope.row) }}
                  </template>
                </el-table-column>
                <el-table-column label="产品大类"
                                 prop="productName"
                                 min-width="100" />
@@ -1013,8 +1038,6 @@
                    <el-input-number v-model="scope.row.deliveryQuantity"
                                     :min="0"
                                     :max="getDeliveryBatchDeliveryMax(scope.row)"
                                     :precision="2"
                                     :step="0.01"
                                     controls-position="right"
                                     @change="handleDeliveryBatchQuantityChange(scope.row)"
                                     style="width: 100%;" />
@@ -1071,6 +1094,7 @@
    tableAmountFormatter,
    formatDecimal,
    buildAmountSummaryFormat,
    roundAmount,
  } from "@/utils/numberFormat";
  import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
@@ -1111,6 +1135,8 @@
    searchForm: {
      customerName: "", // å®¢æˆ·åç§°
      salesContractNo: "", // é”€å”®åˆåŒç¼–号
      projectName: "", // é¡¹ç›®åç§°
      shippingStatus: "", // å‘货状态(未发货/已发货)
      entryDate: null, // å½•入日期
      entryDateStart: undefined,
      entryDateEnd: undefined,
@@ -1214,6 +1240,8 @@
      item?.stockQuantity;
    return quantity ?? 0;
  };
  const formatDeliveryBatchSalesOrderNo = item =>
    item?.salesContractNo || "--";
  const currentStock = ref(null);
  const fetchCurrentStock = async (productModelId) => {
    if (!productModelId) {
@@ -1221,7 +1249,7 @@
      return;
    }
    try {
      const res = await getStockInventoryByModelId(productModelId);
      const res = await getStockInventoryByModelId(productModelId, currentId.value);
      const rawList = Array.isArray(res?.data)
        ? res.data
        : res?.data?.records || res?.data?.rows || [];
@@ -1229,7 +1257,7 @@
      rawList.forEach(item => {
        total += Number(getDeliveryBatchQuantity(item) || 0);
      });
      currentStock.value = total;
      currentStock.value = roundAmount(total);
    } catch (e) {
      console.error(e);
      currentStock.value = 0;
@@ -1238,35 +1266,45 @@
  const getCurrentDeliveryRowQuantity = () => {
    return Number(currentDeliveryRow.value?.noQuantity || 0);
  };
  const selectedDeliveryQuantity = computed(() => {
    return roundAmount(
      (deliveryForm.value.batchNoList || []).reduce(
        (sum, item) => sum + Number(item?.deliveryQuantity || 0),
        0
      )
    );
  });
  const getDeliveryBatchDeliveryMax = row => {
    const productQuantity = getCurrentDeliveryRowQuantity();
    const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0);
    const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce(
    const otherBatchTotal = roundAmount(
      (deliveryForm.value.batchNoList || []).reduce(
      (sum, item) => {
        if (item?.id === row?.id) return sum;
        return sum + Number(item?.deliveryQuantity || 0);
      },
      0
      )
    );
    const remainingProductQuantity = Math.max(
      0,
      productQuantity - otherBatchTotal
    const remainingProductQuantity = roundAmount(
      Math.max(0, productQuantity - otherBatchTotal)
    );
    return Math.max(0, Math.min(batchQuantity, remainingProductQuantity));
  };
  const handleDeliveryBatchQuantityChange = row => {
    const productQuantity = getCurrentDeliveryRowQuantity();
    const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0);
    const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce(
    const otherBatchTotal = roundAmount(
      (deliveryForm.value.batchNoList || []).reduce(
      (sum, item) => {
        if (item?.id === row?.id) return sum;
        return sum + Number(item?.deliveryQuantity || 0);
      },
      0
      )
    );
    const remainingProductQuantity = Math.max(
      0,
      productQuantity - otherBatchTotal
    const remainingProductQuantity = roundAmount(
      Math.max(0, productQuantity - otherBatchTotal)
    );
    const currentValue = Number(row?.deliveryQuantity || 0);
@@ -1283,9 +1321,9 @@
      item => Number(item?.deliveryQuantity || 0) > 0
    );
  };
  const getDeliveryBatchNoList = async productModelId => {
  const getDeliveryBatchNoList = async (productModelId, salesLedgerId) => {
    if (!productModelId) return [];
    const res = await getStockInventoryByModelId(productModelId);
    const res = await getStockInventoryByModelId(productModelId, salesLedgerId);
    const rawList = Array.isArray(res?.data)
      ? res.data
      : res?.data?.records || res?.data?.rows || [];
@@ -2476,7 +2514,8 @@
    currentDeliveryRow.value = row;
    const batchNoList = await getDeliveryBatchNoList(
      row.productModelId || row.modelId
      row.productModelId || row.modelId,
      row.salesLedgerId
    );
    deliveryForm.value = {
      shippingCarNumber: "",
@@ -2499,9 +2538,11 @@
          proxy.$modal.msgWarning("请至少填写一个批号的发货数量");
          return;
        }
        const totalDeliveryQuantity = selectedBatchRows.reduce(
        const totalDeliveryQuantity = roundAmount(
          selectedBatchRows.reduce(
          (sum, item) => sum + Number(item.deliveryQuantity || 0),
          0
          )
        );
        const currentRowNoQuantity = Number(
          currentDeliveryRow.value?.noQuantity || 0