yyb
5 天以前 2d86dc4adc12cc5cd88cee25b8a69d75ae4a10f1
销售订单模板
已添加1个文件
已修改2个文件
461 ■■■■■ 文件已修改
src/views/salesManagement/salesLedger/components/processCardPrint.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/salesOrderPrint.js 437 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>`
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);
  };
};
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("正在获取生产流程卡数据,请稍候...");
    const loadingText =
        command === "salesOrder"
            ? "正在获取销售订单数据,请稍候..."
            : "正在获取生产流程卡数据,请稍候...";
    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();