From f541524cb8f311ac0b7c8771621d361d31cd24bb Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 27 三月 2026 16:19:42 +0800
Subject: [PATCH] 销售发货单

---
 src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js |  276 ++++++++++++++++++++++++++++++++++++++++++++++
 src/api/salesManagement/salesLedger.js                                 |   11 +
 src/views/salesManagement/salesLedger/index.vue                        |   65 +++++++++-
 3 files changed, 346 insertions(+), 6 deletions(-)

diff --git a/src/api/salesManagement/salesLedger.js b/src/api/salesManagement/salesLedger.js
index fe778af..0f176c8 100644
--- a/src/api/salesManagement/salesLedger.js
+++ b/src/api/salesManagement/salesLedger.js
@@ -176,4 +176,15 @@
     url: `/sales/ledger/processCard/${salesLedgerId}`,
     method: "get",
   });
+}
+
+// 鎵撳嵃閿�鍞彂璐у崟
+export function getSalesInvoices(query) {
+  const data =
+    query && typeof query === "object" ? query : { salesLedgerId: query };
+  return request({
+    url: "/sales/ledger/salesInvoices",
+    method: "post",
+    data,
+  });
 }
\ No newline at end of file
diff --git a/src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js b/src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js
new file mode 100644
index 0000000..e23fd53
--- /dev/null
+++ b/src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js
@@ -0,0 +1,276 @@
+const PRINT_TITLE = "閿�鍞彂璐у崟";
+
+const escapeHtml = (value) =>
+  String(value ?? "")
+    .replaceAll("&", "&amp;")
+    .replaceAll("<", "&lt;")
+    .replaceAll(">", "&gt;")
+    .replaceAll('"', "&quot;")
+    .replaceAll("'", "&#39;");
+
+const toNumber = (value) => {
+  const num = Number(value);
+  return Number.isFinite(num) ? num : 0;
+};
+
+const formatDisplayDate = (value) => {
+  if (!value) return "";
+  const date = new Date(value);
+  if (Number.isNaN(date.getTime())) return String(value);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+  return `${year}/${month}/${day}`;
+};
+
+const getItemArea = (item) => toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea);
+
+const getOrderNo = (data, row, item) =>
+  item?.salesContractNo || item?.orderNo || data?.salesContractNo || row?.salesContractNo || "";
+
+const splitItemsByPage = (items, pageSize) => {
+  const list = Array.isArray(items) ? items : [];
+  if (list.length === 0) return [[]];
+  const pages = [];
+  for (let i = 0; i < list.length; i += pageSize) {
+    pages.push(list.slice(i, i + pageSize));
+  }
+  return pages;
+};
+
+const normalizeInvoiceData = (raw, selectedRow) => {
+  const data = raw ?? {};
+  const groups = Array.isArray(data.groups) ? data.groups : [];
+  if (!groups.length) return data;
+
+  const items = groups.flatMap((group) =>
+    (Array.isArray(group?.items) ? group.items : []).map((item) => ({
+      ...item,
+      productDescription: group?.productName || item?.productDescription || "",
+      salesContractNo: group?.salesContractNo || item?.salesContractNo || "",
+      widthHeight: item?.widthHeight || "",
+    }))
+  );
+
+  return {
+    ...data,
+    items,
+    customerName: data.customerName || selectedRow?.customerName || "",
+    contactPerson: data.contactPerson || selectedRow?.contactPerson || "",
+    contactPhone: data.contactPhone || selectedRow?.contactPhone || "",
+    deliveryAddress:
+      data.companyAddress || data.deliveryAddress || data.shippingAddress || selectedRow?.deliveryAddress || "",
+    shipmentNo: data.externalOrderNo || data.shipmentNo || "",
+    register: data.orderMaker || data.register || selectedRow?.entryPersonName || "",
+    registerDate: data.executionDate || data.registerDate || data.entryDate || selectedRow?.entryDate || "",
+  };
+};
+
+const groupByProduct = (items, data, row) => {
+  const list = Array.isArray(items) ? items : [];
+  const map = new Map();
+  list.forEach((item) => {
+    const key = `${item?.productDescription || ""}__${getOrderNo(data, row, item)}`;
+    if (!map.has(key)) {
+      map.set(key, {
+        productName: item?.productDescription || "",
+        orderNo: getOrderNo(data, row, item),
+        items: [],
+      });
+    }
+    map.get(key).items.push(item);
+  });
+  return Array.from(map.values());
+};
+
+const renderItemRows = (items, startIndex) =>
+  items
+    .map((item, idx) => {
+      const sizeText = item?.widthHeight
+        ? escapeHtml(item.widthHeight)
+        : item?.width || item?.height
+          ? `${escapeHtml(item?.width)} * ${escapeHtml(item?.height)}`
+          : "";
+      return `
+      <tr>
+        <td>${startIndex + idx + 1}</td>
+        <td class="left">${escapeHtml(item?.floorCode)}</td>
+        <td>${sizeText}</td>
+        <td>${toNumber(item?.quantity) || ""}</td>
+        <td>${getItemArea(item) ? getItemArea(item).toFixed(2) : ""}</td>
+        <td class="left">${escapeHtml(item?.remark)}</td>
+        <td class="left">${escapeHtml(item?.processRequirement)}</td>
+      </tr>
+    `;
+    })
+    .join("");
+
+export const printSalesDeliveryNote = (rawData, selectedRow = {}) => {
+  const data = normalizeInvoiceData(rawData, selectedRow);
+  const allItems = Array.isArray(data.items) ? data.items : [];
+  const pageSize = 18;
+  const itemPages = splitItemsByPage(allItems, pageSize);
+  const totalPages = itemPages.length;
+
+  const printWindow = window.open("", "_blank", "width=1200,height=900");
+  if (!printWindow) {
+    throw new Error("娴忚鍣ㄦ嫤鎴簡寮圭獥锛岃鍏佽寮圭獥鍚庨噸璇�");
+  }
+
+  const html = `
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <title>${PRINT_TITLE}</title>
+    <style>
+      body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #222; }
+      .page { width: 198mm; margin: 0 auto; padding: 4mm 4mm 6mm; box-sizing: border-box; page-break-after: always; }
+      .page:last-child { page-break-after: auto; }
+      .head-top {
+        display: grid;
+        grid-template-columns: 1fr auto 1fr;
+        align-items: end;
+        margin-bottom: 1px;
+      }
+      .factory {
+        grid-column: 2;
+        text-align: center;
+        font-size: 20px;
+        font-weight: 700;
+        line-height: 1.2;
+      }
+      .page-mark {
+        grid-column: 3;
+        justify-self: end;
+        font-size: 12px;
+        margin-right: 8mm;
+        margin-bottom: 1px;
+      }
+      .head-mid {
+        display: grid;
+        grid-template-columns: 1fr auto 1fr;
+        align-items: center;
+        margin-bottom: 2px;
+      }
+      .head-mid-left { font-size: 13px; text-align: left; }
+      .head-mid-title { font-size: 20px; font-weight: 700; text-align: center; }
+      .head-mid-right { font-size: 13px; text-align: right; padding-right: 8mm; }
+      table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; }
+      td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; }
+      .left { text-align: left; }
+      .group-title td { font-weight: 700; }
+      .subtotal td, .total-row td { font-weight: 700; }
+      .empty td { height: 120px; color: #666; }
+      .footer { margin-top: 6px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; font-size: 13px; }
+      @media print {
+        @page { size: A4 portrait; margin: 8mm; }
+        .page { width: 100%; margin: 0; padding: 0; }
+      }
+    </style>
+  </head>
+  <body>
+  ${itemPages
+    .map((pageItems, pageIndex) => {
+      const pageGroups = groupByProduct(pageItems, data, selectedRow);
+      let serial = pageIndex * pageSize;
+      const totalQty = toNumber(data.totalQuantity) || allItems.reduce((s, it) => s + toNumber(it?.quantity), 0);
+      const totalArea = toNumber(data.totalArea) || allItems.reduce((s, it) => s + getItemArea(it), 0);
+      return `
+    <div class="page">
+      <div class="head-top">
+        <div></div>
+        <div class="factory">楣ゅ澶╂矏閽㈠寲鐜荤拑鍘�</div>
+        <div class="page-mark">绗�${pageIndex + 1}椤�,鍏�${totalPages}椤�</div>
+      </div>
+      <div class="head-mid">
+        <div class="head-mid-left">瀵规柟鍗曞彿: ${escapeHtml(data.deliveryNo || data.shippingNo || selectedRow.expressNumber || "")}</div>
+        <div class="head-mid-title">閿�鍞彂璐у崟</div>
+        <div class="head-mid-right">鍙戣揣鍗曞彿: ${escapeHtml(data.shipmentNo || data.deliveryNo || "")}</div>
+      </div>
+      <table>
+        <tr>
+          <td class="left" colspan="4">瀹㈡埛鍚嶇О: ${escapeHtml(data.customerName || selectedRow.customerName || "")}</td>
+          <td class="left" colspan="3">鑱旂郴浜�: ${escapeHtml(data.contactPerson || selectedRow.contactPerson || "")}</td>
+        </tr>
+        <tr>
+          <td class="left" colspan="4">鍙戣揣鍦板潃: ${escapeHtml(data.deliveryAddress || data.shippingAddress || selectedRow.deliveryAddress || "")}</td>
+          <td class="left" colspan="3">鑱旂郴鐢佃瘽: ${escapeHtml(data.contactPhone || selectedRow.contactPhone || "")}</td>
+        </tr>
+        <tr>
+          <th style="width:8%;">搴忓彿</th>
+          <th style="width:22%;">妤煎眰缂栧彿</th>
+          <th style="width:20%;">瀹�(寮ч暱)*楂�</th>
+          <th style="width:10%;">鏁伴噺</th>
+          <th style="width:12%;">闈㈢Н</th>
+          <th style="width:10%;">澶囨敞</th>
+          <th style="width:18%;">鍔犲伐瑕佹眰</th>
+        </tr>
+        ${
+          pageGroups.length
+            ? pageGroups
+                .map((group) => {
+                  const subQty = group.items.reduce((s, it) => s + toNumber(it?.quantity), 0);
+                  const subArea = group.items.reduce((s, it) => s + getItemArea(it), 0);
+                  const rows = renderItemRows(group.items, serial);
+                  serial += group.items.length;
+                  return `
+          <tr class="group-title">
+            <td colspan="5" class="left">浜у搧鍚嶇О: ${escapeHtml(group.productName)}</td>
+            <td colspan="2" class="left">璁㈠崟缂栧彿: ${escapeHtml(group.orderNo)}</td>
+          </tr>
+          ${rows}
+          <tr class="subtotal">
+            <td colspan="3">灏忚:</td>
+            <td>${subQty || ""}</td>
+            <td>${subArea ? subArea.toFixed(2) : ""}</td>
+            <td colspan="2"></td>
+          </tr>
+                  `;
+                })
+                .join("")
+            : `<tr class="empty"><td colspan="7">鏆傛棤鏄庣粏</td></tr>`
+        }
+        ${
+          pageIndex === totalPages - 1
+            ? `
+        <tr class="total-row">
+          <td colspan="3">鍚堣:</td>
+          <td>${totalQty || ""}</td>
+          <td>${totalArea ? totalArea.toFixed(2) : ""}</td>
+          <td colspan="2"></td>
+        </tr>
+            `
+            : ""
+        }
+      </table>
+      ${
+        pageIndex === totalPages - 1
+          ? `
+      <div class="footer">
+        <div>鍒� 鍗� 鍛�: ${escapeHtml(data.register || selectedRow.entryPersonName || "")}</div>
+        <div>鍒跺崟鏃ユ湡: ${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate || selectedRow.entryDate))}</div>
+        <div>瀹㈡埛绛惧瓧:</div>
+        <div>绛炬敹鏃ユ湡:</div>
+      </div>
+          `
+          : ""
+      }
+    </div>
+      `;
+    })
+    .join("")}
+  </body>
+</html>
+`;
+
+  printWindow.document.write(html);
+  printWindow.document.close();
+  printWindow.onload = () => {
+    setTimeout(() => {
+      printWindow.focus();
+      printWindow.print();
+      printWindow.close();
+    }, 300);
+  };
+};
diff --git a/src/views/salesManagement/salesLedger/index.vue b/src/views/salesManagement/salesLedger/index.vue
index 6254645..08b9b34 100644
--- a/src/views/salesManagement/salesLedger/index.vue
+++ b/src/views/salesManagement/salesLedger/index.vue
@@ -53,6 +53,7 @@
               <el-dropdown-menu>
                 <el-dropdown-item command="finishedProcessCard">鐢熶骇娴佺▼鍗★紙鎴愬搧锛�</el-dropdown-item>
                 <el-dropdown-item command="salesOrder">閿�鍞鍗�</el-dropdown-item>
+                <el-dropdown-item command="salesDeliveryNote">閿�鍞彂璐у崟</el-dropdown-item>
               </el-dropdown-menu>
             </template>
           </el-dropdown>
@@ -891,6 +892,7 @@
 	saleProcessBind,
 	getSaleProcessBindInfo,
 	getProcessCard,
+	getSalesInvoices,
 } from "@/api/salesManagement/salesLedger.js";
 import { modelList, productTreeList } from "@/api/basicData/product.js";
 import useFormData from "@/hooks/useFormData.js";
@@ -898,6 +900,7 @@
 import { getCurrentDate } from "@/utils/index.js";
 import { printFinishedProcessCard } from "./components/processCardPrint.js";
 import { printSalesOrder } from "./components/salesOrderPrint.js";
+import { printSalesDeliveryNote } from "./components/salesDeliveryPrint.js";
 // import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js";
 
 const userStore = useUserStore();
@@ -2025,14 +2028,53 @@
 };
 
 const handlePrintCommand = async (command) => {
-	if (command !== "finishedProcessCard" && command !== "salesOrder") return;
-	if (selectedRows.value.length !== 1) {
+	if (command !== "finishedProcessCard" && command !== "salesOrder" && command !== "salesDeliveryNote") return;
+	if (command === "salesDeliveryNote") {
+		if (selectedRows.value.length === 0) {
+			proxy.$modal.msgWarning("璇疯嚦灏戦�夋嫨涓�鏉¢攢鍞彴璐︽暟鎹繘琛屾墦鍗�");
+			return;
+		}
+		const customerNames = Array.from(
+			new Set(selectedRows.value.map((item) => String(item?.customerName ?? "").trim()))
+		);
+		if (customerNames.length > 1) {
+			proxy.$modal.msgWarning("浠呮敮鎸佺浉鍚屽鎴峰悕绉扮殑閿�鍞彴璐﹀悎骞跺彂璐ф墦鍗�");
+			return;
+		}
+	} else if (selectedRows.value.length !== 1) {
 		proxy.$modal.msgWarning("璇烽�夋嫨涓�鏉¢攢鍞彴璐︽暟鎹繘琛屾墦鍗�");
 		return;
 	}
 
 	const selectedRow = selectedRows.value[0];
 	const selectedId = selectedRow?.id;
+	if (command === "salesDeliveryNote") {
+		const selectedIds = selectedRows.value
+			.map((item) => item?.id)
+			.filter((id) => id !== null && id !== undefined && id !== "");
+		if (selectedIds.length !== selectedRows.value.length) {
+			proxy.$modal.msgWarning("褰撳墠閫夋嫨鏁版嵁瀛樺湪缂哄皯ID鐨勮褰曪紝鏃犳硶鎵撳嵃");
+			return;
+		}
+		const loadingText =
+			command === "salesOrder"
+				? "姝e湪鑾峰彇閿�鍞鍗曟暟鎹紝璇风◢鍊�..."
+				: command === "salesDeliveryNote"
+					? "姝e湪鑾峰彇閿�鍞彂璐у崟鏁版嵁锛岃绋嶅��..."
+					: "姝e湪鑾峰彇鐢熶骇娴佺▼鍗℃暟鎹紝璇风◢鍊�...";
+		proxy.$modal.loading(loadingText);
+		try {
+			const res = await getSalesInvoices(selectedIds);
+			const salesInvoiceData = res?.data ?? {};
+			printSalesDeliveryNote(salesInvoiceData, selectedRow);
+		} catch (error) {
+			console.error("鎵撳嵃閿�鍞彂璐у崟澶辫触:", error);
+			proxy.$modal.msgError("鎵撳嵃澶辫触锛岃绋嶅悗閲嶈瘯");
+		} finally {
+			proxy.$modal.closeLoading();
+		}
+		return;
+	}
 	if (!selectedId) {
 		proxy.$modal.msgWarning("褰撳墠閫夋嫨鏁版嵁缂哄皯ID锛屾棤娉曟墦鍗�");
 		return;
@@ -2041,18 +2083,29 @@
 	const loadingText =
 		command === "salesOrder"
 			? "姝e湪鑾峰彇閿�鍞鍗曟暟鎹紝璇风◢鍊�..."
-			: "姝e湪鑾峰彇鐢熶骇娴佺▼鍗℃暟鎹紝璇风◢鍊�...";
+			: command === "salesDeliveryNote"
+				? "姝e湪鑾峰彇閿�鍞彂璐у崟鏁版嵁锛岃绋嶅��..."
+				: "姝e湪鑾峰彇鐢熶骇娴佺▼鍗℃暟鎹紝璇风◢鍊�...";
 	proxy.$modal.loading(loadingText);
 	try {
-		const res = await getProcessCard(selectedId);
-		const processCardData = res?.data ?? {};
 		if (command === "salesOrder") {
+			const res = await getProcessCard(selectedId);
+			const processCardData = res?.data ?? {};
 			printSalesOrder(processCardData);
 		} else {
+			const res = await getProcessCard(selectedId);
+			const processCardData = res?.data ?? {};
 			printFinishedProcessCard(processCardData);
 		}
 	} catch (error) {
-		console.error(command === "salesOrder" ? "鎵撳嵃閿�鍞鍗曞け璐�:" : "鎵撳嵃鐢熶骇娴佺▼鍗″け璐�:", error);
+		console.error(
+			command === "salesOrder"
+				? "鎵撳嵃閿�鍞鍗曞け璐�:"
+				: command === "salesDeliveryNote"
+					? "鎵撳嵃閿�鍞彂璐у崟澶辫触:"
+					: "鎵撳嵃鐢熶骇娴佺▼鍗″け璐�:",
+			error
+		);
 		proxy.$modal.msgError("鎵撳嵃澶辫触锛岃绋嶅悗閲嶈瘯");
 	} finally {
 		proxy.$modal.closeLoading();

--
Gitblit v1.9.3