From 2d86dc4adc12cc5cd88cee25b8a69d75ae4a10f1 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 27 三月 2026 15:58:53 +0800
Subject: [PATCH] 销售订单模板

---
 src/views/salesManagement/salesLedger/components/salesOrderPrint.js  |  437 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/views/salesManagement/salesLedger/components/processCardPrint.js |    4 
 src/views/salesManagement/salesLedger/index.vue                      |   20 +
 3 files changed, 454 insertions(+), 7 deletions(-)

diff --git a/src/views/salesManagement/salesLedger/components/processCardPrint.js b/src/views/salesManagement/salesLedger/components/processCardPrint.js
index d89ee18..d1c0472 100644
--- a/src/views/salesManagement/salesLedger/components/processCardPrint.js
+++ b/src/views/salesManagement/salesLedger/components/processCardPrint.js
@@ -231,12 +231,12 @@
           ? `<div class="footer">
         <div class="footer-row">
           <div class="footer-item">鍒跺崟鍛�:${escapeHtml(data.register)}</div>
-          <div class="footer-item">瀹℃牳鍛�:__________</div>
+          <div class="footer-item">瀹℃牳鍛�:${escapeHtml(data.register)}</div>
           <div class="footer-item">宸ヨ壓鍛�:${escapeHtml(data.technician ?? "")}</div>
         </div>
         <div class="footer-row">
           <div class="footer-item">鍒跺崟鏃ユ湡:${escapeHtml(formatDisplayDate(data.registerDate))}</div>
-          <div class="footer-item">瀹℃牳鏃ユ湡:__________</div>
+          <div class="footer-item">瀹℃牳鏃ユ湡:${escapeHtml(formatDisplayDate(data.registerDate))}</div>
           <div class="footer-item">鎵撳嵃鏃ユ湡:${getCurrentDate()}</div>
         </div>
       </div>`
diff --git a/src/views/salesManagement/salesLedger/components/salesOrderPrint.js b/src/views/salesManagement/salesLedger/components/salesOrderPrint.js
new file mode 100644
index 0000000..9e60eab
--- /dev/null
+++ b/src/views/salesManagement/salesLedger/components/salesOrderPrint.js
@@ -0,0 +1,437 @@
+const PRINT_TITLE = "閿�鍞鍗�";
+
+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 getCurrentDateTime = () => {
+  const date = new Date();
+  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 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 formatMoney = (value) => {
+  const num = toNumber(value);
+  return num.toFixed(2);
+};
+
+const formatArea = (value) => {
+  const num = toNumber(value);
+  return num ? num.toFixed(2) : "";
+};
+
+const formatQty = (value) => {
+  const num = toNumber(value);
+  return num ? String(num) : "";
+};
+
+const getItemAmount = (item) => {
+  const fromPrice = toNumber(item?.taxInclusiveUnitPrice) * toNumber(item?.quantity);
+  const amount = toNumber(item?.taxInclusiveTotalPrice || item?.amount || fromPrice);
+  return amount;
+};
+
+const getItemArea = (item) =>
+  toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea);
+
+const getDeliveryAddress = (data) =>
+  data?.deliveryAddress || data?.shippingAddress || data?.address || data?.shipAddress || "";
+
+const normalizeRequirementText = (value) => {
+  const text = String(value ?? "").trim();
+  if (!text) return "";
+  return text
+    .replace(/^鍔犲伐瑕佹眰鍜屽娉╗:锛歖\s*/g, "")
+    .replace(/^鍔犲伐瑕佹眰[:锛歖\s*/g, "")
+    .replace(/^澶囨敞[:锛歖\s*/g, "");
+};
+
+const extractOtherFees = (data, items) => {
+  const source = [];
+  if (Array.isArray(data?.otherAmounts)) source.push(...data.otherAmounts);
+  if (Array.isArray(data?.otherAmountList)) source.push(...data.otherAmountList);
+  if (Array.isArray(data?.otherAmountProjects)) source.push(...data.otherAmountProjects);
+  if (Array.isArray(data?.salesProductProcessList)) source.push(...data.salesProductProcessList);
+  (Array.isArray(items) ? items : []).forEach((item) => {
+    if (Array.isArray(item?.salesProductProcessList)) source.push(...item.salesProductProcessList);
+    if (Array.isArray(item?.otherAmounts)) source.push(...item.otherAmounts);
+  });
+
+  const map = new Map();
+  source.forEach((fee) => {
+    const name = String(fee?.processName || fee?.name || fee?.itemName || "").trim();
+    if (!name) return;
+    const quantity = toNumber(fee?.quantity || fee?.num);
+    const unitPrice = toNumber(fee?.unitPrice || fee?.price);
+    const amount =
+      toNumber(fee?.amount || fee?.totalPrice) || (quantity && unitPrice ? quantity * unitPrice : 0);
+    const key = name;
+    if (!map.has(key)) {
+      map.set(key, { name, quantity: 0, unitPrice: 0, amount: 0 });
+    }
+    const row = map.get(key);
+    row.quantity += quantity;
+    row.unitPrice = unitPrice || row.unitPrice;
+    row.amount += amount;
+  });
+  return Array.from(map.values());
+};
+
+const renderOtherFeeNames = (rows) =>
+  (Array.isArray(rows) ? rows : [])
+    .map((row) => escapeHtml(row.name))
+    .filter(Boolean)
+    .join("<br/>");
+
+const renderOtherFeeUnitPrices = (rows) =>
+  (Array.isArray(rows) ? rows : [])
+    .map((row) => (row.unitPrice ? formatMoney(row.unitPrice) : ""))
+    .filter(Boolean)
+    .join("<br/>");
+
+const renderOtherFeeQuantities = (rows) =>
+  (Array.isArray(rows) ? rows : [])
+    .map((row) => (row.quantity ? String(row.quantity) : ""))
+    .filter(Boolean)
+    .join("<br/>");
+
+const renderOtherFeeAmounts = (rows) =>
+  (Array.isArray(rows) ? rows : [])
+    .map((row) => (row.amount ? formatMoney(row.amount) : ""))
+    .filter(Boolean)
+    .join("<br/>");
+
+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 renderRows = (items, startIndex) => {
+  const list = Array.isArray(items) ? items : [];
+  if (list.length === 0) {
+    return `
+      <tr>
+        <td colspan="8" class="empty">鏆傛棤鏄庣粏</td>
+      </tr>
+    `;
+  }
+  return list
+    .map((item, idx) => {
+      const width = escapeHtml(item?.width);
+      const height = escapeHtml(item?.height);
+      const sizeText = width || height ? `${width}*${height}` : "";
+      const unitPrice = formatMoney(item?.taxInclusiveUnitPrice || item?.unitPrice);
+      const amount = formatMoney(getItemAmount(item));
+      return `
+      <tr>
+        <td>${startIndex + idx + 1}</td>
+        <td>${escapeHtml(item?.floorCode)}</td>
+        <td>${sizeText}</td>
+        <td>${formatQty(item?.quantity)}</td>
+        <td>${formatArea(item?.area || item?.settleTotalArea || item?.actualTotalArea)}</td>
+        <td>${unitPrice}</td>
+        <td>${amount}</td>
+        <td>${escapeHtml(item?.processRequirement)}</td>
+      </tr>
+    `;
+    })
+    .join("");
+};
+
+export const printSalesOrder = (orderData) => {
+  const data = orderData ?? {};
+  const items = Array.isArray(data.items) ? data.items : [];
+  const pageSize = 15;
+  const pages = splitItemsByPage(items, pageSize);
+  const totalPages = pages.length;
+
+  const totalQuantity = toNumber(data.totalQuantity) || items.reduce((sum, item) => sum + toNumber(item?.quantity), 0);
+  const totalArea = toNumber(data.totalArea) || items.reduce((sum, item) => sum + getItemArea(item), 0);
+  const totalAmount = items.reduce((sum, item) => sum + getItemAmount(item), 0);
+  const otherFees = extractOtherFees(data, items);
+
+  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: #111; }
+      .page {
+        width: 198mm;
+        min-height: 186mm;
+        margin: 0 auto;
+        padding: 5mm 4mm 16mm;
+        box-sizing: border-box;
+        page-break-after: always;
+        position: relative;
+      }
+      .page:last-child { page-break-after: auto; }
+      .title-main { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 1px; line-height: 1.15; margin-top: 1mm; }
+      .title-sub { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 6px; line-height: 1.15; margin: 1mm 0 4mm; }
+      table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; }
+      .detail-table {
+        border-right: 1px solid #222 !important;
+        box-shadow: inset -1px 0 0 #222;
+      }
+      .detail-table-wrap {
+        position: relative;
+        margin-top: -1px;
+      }
+      .detail-table-wrap::after {
+        content: "";
+        position: absolute;
+        top: 0;
+        right: 0;
+        width: 1px;
+        height: 100%;
+        background: #222;
+        pointer-events: none;
+      }
+      .sheet-wrap {
+        position: relative;
+        overflow: visible;
+      }
+      .sheet-wrap::after {
+        content: "";
+        position: absolute;
+        top: 0;
+        right: -0.4px;
+        width: 1.2px;
+        height: 100%;
+        background: #222;
+        pointer-events: none;
+        z-index: 20;
+      }
+      td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; }
+      tr > td:first-child, tr > th:first-child { border-left: 1px solid #222 !important; }
+      tr > td:last-child, tr > th:last-child { border-right: 1px solid #222 !important; }
+      .left { text-align: left; }
+      .bold { font-weight: 700; }
+      .cell-title { font-weight: 700; white-space: nowrap; }
+      .product-row td { font-size: 13px; font-weight: 700; }
+      .empty { height: 140px; color: #777; }
+      .large-row td { height: 46px; vertical-align: top; }
+      .total-row td { font-weight: 700; font-size: 13px; line-height: 1.2; }
+      .footer { margin-top: 5px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; font-size: 13px; line-height: 1.7; }
+      .footer b { display: inline-block; min-width: 74px; }
+      .customer-sign-row {
+        margin-top: 6px;
+        display: grid;
+        grid-template-columns: 1fr 1fr 1fr;
+      }
+      .customer-sign-cell { text-align: left; }
+      .customer-sign { font-size: 13px; font-weight: 700; line-height: 1.2; }
+      .right { text-align: right; }
+      .footer-page-right { display: block; text-align: right; font-size: 12px; }
+      .footer-pair {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        gap: 8px;
+      }
+      .other-fee-cell { white-space: normal; word-break: break-all; line-height: 1.5; vertical-align: top; }
+      .other-fee-header { font-weight: 700; }
+      .other-fee-header-grid {
+        display: grid;
+        grid-template-columns: repeat(4, 1fr);
+        align-items: center;
+        text-align: center;
+      }
+      .other-fee-header-row td { border-bottom: none !important; }
+      .other-fee-content-row td { border-top: none !important; }
+      @media print {
+        @page { size: A4 landscape; margin: 6mm; }
+        .page { width: 100%; margin: 0; padding: 0 0 14mm; min-height: 0; }
+      }
+    </style>
+  </head>
+  <body>
+    ${pages
+      .map((pageItems, pageIndex) => {
+        const isLastPage = pageIndex === totalPages - 1;
+        const startIndex = pageIndex * pageSize;
+        return `
+    <div class="page">
+      <div class="title-main">楣ゅ澶╂矏閽㈠寲鐜荤拑鍘�</div>
+      <div class="title-sub">閿�鍞鍗�</div>
+
+      <div class="sheet-wrap">
+      <table>
+        <colgroup>
+          <col style="width: 14%;" />
+          <col style="width: 20%;" />
+          <col style="width: 20%;" />
+          <col style="width: 14%;" />
+          <col style="width: 32%;" />
+        </colgroup>
+        <tr>
+          <td class="cell-title">瀹㈡埛鍚嶇О:</td>
+          <td class="left">${escapeHtml(data.customerName)}</td>
+          <td class="cell-title">椤圭洰鍚嶇О:</td>
+          <td class="left">${escapeHtml(data.projectName)}</td>
+          <td class="left"><span class="cell-title">涓� 鍔� 鍛�:</span> ${escapeHtml(data.salesman)}</td>
+        </tr>
+        <tr>
+          <td class="cell-title">鍒跺崟鏃ユ湡:</td>
+          <td class="left">${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate))}</td>
+          <td class="cell-title">浜よ揣鏃ユ湡:</td>
+          <td class="left">${escapeHtml(formatDisplayDate(data.deliveryDate))}</td>
+          <td class="left"></td>
+        </tr>
+        <tr>
+          <td class="cell-title">閫佽揣鍦板潃:</td>
+          <td colspan="4" class="left">${escapeHtml(getDeliveryAddress(data))}</td>
+        </tr>
+      </table>
+
+      <div class="detail-table-wrap">
+      <table class="detail-table">
+        <colgroup>
+          <col style="width: 9%;" />
+          <col style="width: 12%;" />
+          <col style="width: 12%;" />
+          <col style="width: 7%;" />
+          <col style="width: 10%;" />
+          <col style="width: 8%;" />
+          <col style="width: 10%;" />
+          <col style="width: 32%;" />
+        </colgroup>
+        <tr>
+          <th>搴忓彿</th>
+          <th>妤煎眰缂栧彿</th>
+          <th>瀹�(寮ч暱)*楂�</th>
+          <th>鏁伴噺</th>
+          <th>缁撶畻闈㈢Н</th>
+          <th>鍗曚环</th>
+          <th>閲戦</th>
+          <th>鍔犲伐瑕佹眰</th>
+        </tr>
+        <tr class="product-row">
+          <td colspan="6" class="left">浜у搧鍚嶇О: ${escapeHtml(items[0]?.productDescription)}</td>
+          <td colspan="2" class="left">璁㈠崟缂栧彿: ${escapeHtml(data.salesContractNo)}</td>
+        </tr>
+        ${renderRows(pageItems, startIndex)}
+        ${
+          isLastPage
+            ? `
+        <tr class="total-row">
+          <td colspan="3" class="left">灏忚:</td>
+          <td>${totalQuantity || ""}</td>
+          <td>${totalArea ? totalArea.toFixed(2) : ""}</td>
+          <td></td>
+          <td>${formatMoney(totalAmount)}</td>
+          <td></td>
+        </tr>
+        <tr class="total-row">
+          <td colspan="3" class="left">鍚堣:</td>
+          <td>${totalQuantity || ""}</td>
+          <td>${totalArea ? totalArea.toFixed(2) : ""}</td>
+          <td></td>
+          <td>${formatMoney(totalAmount)}</td>
+          <td></td>
+        </tr>
+        <tr class="other-fee-header-row">
+          <td colspan="5" class="left other-fee-header">
+            <div class="other-fee-header-grid">
+              <span>鍏朵粬璐圭敤</span>
+              <span>鍗曚环</span>
+              <span>鏁伴噺</span>
+              <span>閲戦</span>
+            </div>
+          </td>
+          <td colspan="3" class="left other-fee-header">鍔犲伐瑕佹眰鍜屽娉�:</td>
+        </tr>
+        <tr class="large-row other-fee-content-row">
+          <td colspan="5" class="left other-fee-cell">
+            ${
+              renderOtherFeeNames(otherFees) ||
+              renderOtherFeeUnitPrices(otherFees) ||
+              renderOtherFeeQuantities(otherFees) ||
+              renderOtherFeeAmounts(otherFees)
+                ? `${renderOtherFeeNames(otherFees)}`
+                : ""
+            }
+          </td>
+          <td colspan="3" class="left other-fee-cell">${escapeHtml(normalizeRequirementText(data.orderProcessRequirement))}</td>
+        </tr>
+        <tr class="total-row">
+          <td colspan="8" class="left">鎬婚噾棰�: ${formatMoney(totalAmount)}鍏�</td>
+        </tr>
+            `
+            : `
+        <tr><td colspan="8" class="right">涓嬮〉缁�...</td></tr>
+            `
+        }
+      </table>
+      </div>
+      </div>
+
+      ${
+        isLastPage
+          ? `
+      <div class="customer-sign-row">
+        <span></span>
+        <span></span>
+        <span class="customer-sign customer-sign-cell">瀹㈡埛绛惧悕:</span>
+      </div>
+      <div class="footer">
+        <div><b>鍒跺崟鍛�:</b>${escapeHtml(data.register)}</div>
+        <div><b>瀹℃牳鍛�:</b>${escapeHtml(data.register)}</div>
+        <div><b>鎵撳嵃浜�:</b>${escapeHtml(data.register)}</div>
+        <div><b>鍒跺崟鏃ユ湡:</b>${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate))}</div>
+        <div><b>瀹℃牳鏃ユ湡:</b>${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate))}</div>
+        <div class="footer-pair"><span><b>鎵撳嵃鏃堕棿:</b>${getCurrentDateTime()}</span><span class="footer-page-right">绗�${pageIndex + 1}椤�,鍏�${totalPages}椤�</span></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 8f69d26..6254645 100644
--- a/src/views/salesManagement/salesLedger/index.vue
+++ b/src/views/salesManagement/salesLedger/index.vue
@@ -51,7 +51,8 @@
             </el-button>
             <template #dropdown>
               <el-dropdown-menu>
-                <el-dropdown-item command="finishedProcessCard">鎵撳嵃鐢熶骇娴佺▼鍗★紙鎴愬搧锛�</el-dropdown-item>
+                <el-dropdown-item command="finishedProcessCard">鐢熶骇娴佺▼鍗★紙鎴愬搧锛�</el-dropdown-item>
+                <el-dropdown-item command="salesOrder">閿�鍞鍗�</el-dropdown-item>
               </el-dropdown-menu>
             </template>
           </el-dropdown>
@@ -896,6 +897,7 @@
 import dayjs from "dayjs";
 import { getCurrentDate } from "@/utils/index.js";
 import { printFinishedProcessCard } from "./components/processCardPrint.js";
+import { printSalesOrder } from "./components/salesOrderPrint.js";
 // import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js";
 
 const userStore = useUserStore();
@@ -2023,7 +2025,7 @@
 };
 
 const handlePrintCommand = async (command) => {
-	if (command !== "finishedProcessCard") return;
+	if (command !== "finishedProcessCard" && command !== "salesOrder") return;
 	if (selectedRows.value.length !== 1) {
 		proxy.$modal.msgWarning("璇烽�夋嫨涓�鏉¢攢鍞彴璐︽暟鎹繘琛屾墦鍗�");
 		return;
@@ -2036,13 +2038,21 @@
 		return;
 	}
 
-	proxy.$modal.loading("姝e湪鑾峰彇鐢熶骇娴佺▼鍗℃暟鎹紝璇风◢鍊�...");
+	const loadingText =
+		command === "salesOrder"
+			? "姝e湪鑾峰彇閿�鍞鍗曟暟鎹紝璇风◢鍊�..."
+			: "姝e湪鑾峰彇鐢熶骇娴佺▼鍗℃暟鎹紝璇风◢鍊�...";
+	proxy.$modal.loading(loadingText);
 	try {
 		const res = await getProcessCard(selectedId);
 		const processCardData = res?.data ?? {};
-		printFinishedProcessCard(processCardData);
+		if (command === "salesOrder") {
+			printSalesOrder(processCardData);
+		} else {
+			printFinishedProcessCard(processCardData);
+		}
 	} catch (error) {
-		console.error("鎵撳嵃鐢熶骇娴佺▼鍗″け璐�:", error);
+		console.error(command === "salesOrder" ? "鎵撳嵃閿�鍞鍗曞け璐�:" : "鎵撳嵃鐢熶骇娴佺▼鍗″け璐�:", error);
 		proxy.$modal.msgError("鎵撳嵃澶辫触锛岃绋嶅悗閲嶈瘯");
 	} finally {
 		proxy.$modal.closeLoading();

--
Gitblit v1.9.3