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("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+
+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