From e57932740c39954cf0baa5d7eaa6c3eaeb313fb4 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期五, 04 九月 2026 11:46:04 +0800
Subject: [PATCH] Merge branch 'dev_pro_河南鹤壁' of http://114.132.189.42:9002/r/product-inventory-management into dev_pro_河南鹤壁

---
 src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue    |   40 ++
 src/views/productionManagement/productionOrder/components/PurchaseLedgerAddDialog.vue |  856 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 896 insertions(+), 0 deletions(-)

diff --git a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
index ca25853..9067dde 100644
--- a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
+++ b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -6,12 +6,19 @@
                @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="100">
           <template #default="{ row }">
@@ -143,6 +150,8 @@
     <ProductSelectDialog v-model="materialProductDialogVisible"
                          @confirm="handleMaterialProductConfirm"
                          single />
+    <PurchaseLedgerAddDialog ref="purchaseLedgerAddDialogRef"
+                             v-model="purchaseLedgerDialogVisible" />
     <!-- request-url="/stockInventory/rawMaterials" -->
   </div>
 </template>
@@ -151,6 +160,7 @@
   import { computed, ref, watch } from "vue";
   import { ElMessage } from "element-plus";
   import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+  import PurchaseLedgerAddDialog from "./PurchaseLedgerAddDialog.vue";
   import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js";
   import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
   import {
@@ -174,6 +184,9 @@
   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;
@@ -378,6 +391,7 @@
 
   const handleClose = () => {
     materialTableData.value = [];
+    selectedMaterialRows.value = [];
     currentMaterialSelectRowIndex.value = -1;
   };
 
@@ -419,6 +433,32 @@
     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 => {
     materialTableData.value.splice(index, 1);
   };
diff --git a/src/views/productionManagement/productionOrder/components/PurchaseLedgerAddDialog.vue b/src/views/productionManagement/productionOrder/components/PurchaseLedgerAddDialog.vue
new file mode 100644
index 0000000..a0da9d4
--- /dev/null
+++ b/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>

--
Gitblit v1.9.3