<script lang="ts" setup>
|
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
|
|
import { computed, nextTick, ref } from 'vue';
|
|
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
import { DICT_TYPE } from '@vben/constants';
|
import { getDictLabel } from '@vben/hooks';
|
import { useUserStore } from '@vben/stores';
|
import { erpPriceMultiply, formatDateTime } from '@vben/utils';
|
|
import { getCustomerSimpleList } from '#/api/crm/customer';
|
import { getAccountSimpleList } from '#/api/erp/finance/account';
|
import { getSaleOrder } from '#/api/erp/sale/order';
|
import { getSimpleUserList } from '#/api/system/user';
|
|
/**
|
* 销售订单打印组件
|
*
|
* 用于导出/打印销售订单详细信息,供企业盖章留存。
|
* 打印样式参考 WMS 出库单打印(wms/order/shipment/modules/print.vue)。
|
*/
|
defineOptions({ name: 'ErpSaleOrderPrint' });
|
|
const userStore = useUserStore();
|
|
const printData = ref<Partial<ErpSaleOrderApi.SaleOrder>>({});
|
/** 销售人员昵称映射 */
|
const saleUserMap = ref<Map<number, string>>(new Map());
|
/** 结算账户名称映射 */
|
const accountMap = ref<Map<number, string>>(new Map());
|
/** 客户名称映射(详情接口不返回客户名称,需按客户编号解析) */
|
const customerMap = ref<Map<number, string>>(new Map());
|
/** 打印时间 */
|
const printTime = ref('');
|
|
/** 订单状态文案(status 为空时不调用 getDictLabel,避免内部对 undefined 执行 toString 报错) */
|
const statusText = computed(() => {
|
if (printData.value.status == null) {
|
return '-';
|
}
|
const label = getDictLabel(DICT_TYPE.ERP_AUDIT_STATUS, printData.value.status);
|
return label || '-';
|
});
|
|
/** 出库状态文案 */
|
const outStatusText = computed(() => {
|
switch (printData.value.outStatus) {
|
case 0:
|
return '未出库';
|
case 1:
|
return '已预留';
|
case 2:
|
return '部分出库';
|
case 3:
|
return '全部出库';
|
default:
|
return '-';
|
}
|
});
|
|
/** 生产状态文案 */
|
const productionStatusText = computed(() => {
|
if (printData.value.needProduction !== 1) {
|
return '无需生产';
|
}
|
return printData.value.productionStatus === 1 ? '已完成' : '未完成';
|
});
|
|
/** 销售人员名称 */
|
const saleUserName = computed(
|
() => saleUserMap.value.get(printData.value.saleUserId!) || '-',
|
);
|
|
/** 结算账户名称 */
|
const accountName = computed(
|
() => accountMap.value.get(printData.value.accountId!) || '-',
|
);
|
|
/** 客户名称:优先取详情返回的客户名称,否则按客户编号解析 */
|
const customerName = computed(
|
() =>
|
printData.value.customerName ||
|
customerMap.value.get(printData.value.customerId!) ||
|
'-',
|
);
|
|
/** 创建人名称:详情接口不返回创建人名称,按创建人编号解析 */
|
const creatorName = computed(
|
() =>
|
printData.value.creatorName ||
|
saleUserMap.value.get(Number(printData.value.creator)) ||
|
'-',
|
);
|
|
/** 单价 × 数量(任一为空返回 undefined) */
|
function multiplyPrice(price?: number, count?: number) {
|
return price == null || count == null ? undefined : erpPriceMultiply(price, count);
|
}
|
|
/**
|
* 明细行(后端不返回明细行的 totalProductPrice/totalPrice,需按单价×数量计算:
|
* 金额 = 单价 × 数量;税额 = 金额 × 税率;含税金额 = 金额 + 税额)
|
*/
|
const printRows = computed(() =>
|
(printData.value.items || []).map((item) => {
|
const totalProductPrice =
|
item.totalProductPrice ?? multiplyPrice(item.productPrice, item.count) ?? 0;
|
const taxPrice =
|
item.taxPrice ?? multiplyPrice(totalProductPrice, (item.taxPercent ?? 0) / 100) ?? 0;
|
const totalPrice = item.totalPrice ?? totalProductPrice + taxPrice;
|
return { ...item, totalProductPrice, taxPrice, totalPrice };
|
}),
|
);
|
|
/** 明细合计:数量/金额优先取订单头(后端权威值),税额与含税金额按明细累加(订单头 totalPrice 为优惠后金额,另在金额信息区展示) */
|
const summaries = computed(() => {
|
const rows = printRows.value;
|
const count = rows.reduce((sum, item) => sum + (item.count || 0), 0);
|
const totalProductPrice = rows.reduce(
|
(sum, item) => sum + (item.totalProductPrice || 0),
|
0,
|
);
|
const taxPrice = rows.reduce((sum, item) => sum + (item.taxPrice || 0), 0);
|
const totalPrice = rows.reduce((sum, item) => sum + (item.totalPrice || 0), 0);
|
return {
|
count: printData.value.totalCount ?? count,
|
totalProductPrice: printData.value.totalProductPrice ?? totalProductPrice,
|
taxPrice,
|
totalPrice,
|
};
|
});
|
|
/** 保留两位小数(金额展示) */
|
function formatPrice(value?: number | null) {
|
return value === undefined || value === null || Number.isNaN(Number(value))
|
? '-'
|
: Number(value).toFixed(2);
|
}
|
|
/** 保留三位小数(数量展示) */
|
function formatCount(value?: number | null) {
|
return value === undefined || value === null || Number.isNaN(Number(value))
|
? '-'
|
: Number(value).toFixed(3);
|
}
|
|
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
function waitForPaint() {
|
return new Promise<void>((resolve) => {
|
requestAnimationFrame(() => {
|
requestAnimationFrame(() => resolve());
|
});
|
});
|
}
|
|
/** 退出打印模式,恢复当前页面显示 */
|
function removePrintMode() {
|
document.body.classList.remove('erp-sale-order-printing');
|
}
|
|
/** 打印销售订单:加载数据后只展示打印区域,再调用浏览器打印 */
|
async function print(id: number) {
|
const [order, userList, accountList, customerList] = await Promise.all([
|
getSaleOrder(id),
|
getSimpleUserList(),
|
getAccountSimpleList(),
|
getCustomerSimpleList(),
|
]);
|
printData.value = order;
|
saleUserMap.value = new Map(
|
userList.map((item) => [Number(item.id), item.nickname || item.username]),
|
);
|
accountMap.value = new Map(
|
accountList.map((item) => [Number(item.id), item.name]),
|
);
|
customerMap.value = new Map(
|
customerList.map((item) => [Number(item.id), item.name]),
|
);
|
printTime.value = formatDateTime(new Date());
|
await nextTick();
|
await waitForPaint();
|
document.body.classList.add('erp-sale-order-printing');
|
window.addEventListener('afterprint', removePrintMode, { once: true });
|
window.print();
|
}
|
|
defineExpose({ print });
|
</script>
|
|
<template>
|
<Teleport to="body">
|
<div
|
id="erpSaleOrderPrint"
|
class="erp-sale-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
>
|
<div class="relative mb-2">
|
<h2 class="m-0 text-center text-[1.6em] font-bold leading-[1.2]">
|
销售订单
|
</h2>
|
<div v-if="printData.no" class="absolute right-0 top-0">
|
<Barcode
|
:content="printData.no"
|
:display-value="false"
|
:format="BarcodeFormatEnum.CODE39"
|
:height="40"
|
:width="180"
|
/>
|
</div>
|
</div>
|
|
<!-- 基本信息 -->
|
<div
|
class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]"
|
>
|
<div>订单单号:{{ printData.no || '-' }}</div>
|
<div>
|
订单时间:{{
|
formatDateTime(printData.orderTime) || '-'
|
}}
|
</div>
|
<div>客户:{{ customerName }}</div>
|
<div>销售人员:{{ saleUserName }}</div>
|
<div>关联合同:{{ printData.contractNo || '-' }}</div>
|
<div>结算账户:{{ accountName }}</div>
|
<div>订单状态:{{ statusText }}</div>
|
<div>出库状态:{{ outStatusText }}</div>
|
<div>生产状态:{{ productionStatusText }}</div>
|
<div>创建人:{{ creatorName }}</div>
|
<div>
|
创建时间:{{
|
formatDateTime(printData.createTime) || '-'
|
}}
|
</div>
|
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
</div>
|
|
<!-- 产品明细 -->
|
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
<thead>
|
<tr>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center font-bold"
|
>
|
序号
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
>
|
产品名称及规格
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center font-bold"
|
>
|
单位
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
|
>
|
数量
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
|
>
|
产品单价(元)
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
|
>
|
金额(元)
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
|
>
|
税率(%)
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
|
>
|
税额(元)
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
|
>
|
含税金额(元)
|
</th>
|
<th
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
>
|
备注
|
</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr v-for="(item, index) in printRows" :key="item.id || index">
|
<td class="border border-solid border-[#dcdfe6] p-2 text-center">
|
{{ index + 1 }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2">
|
<div>{{ item.productName || '-' }}</div>
|
<div v-if="item.productSpecification" class="text-xs">
|
{{ item.productSpecification }}
|
</div>
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2 text-center">
|
{{ item.productUnitName || '-' }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
{{ formatCount(item.count) }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
{{ formatPrice(item.productPrice) }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
{{ formatPrice(item.totalProductPrice) }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
{{ formatPrice(item.taxPercent) }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
{{ formatPrice(item.taxPrice) }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
{{ formatPrice(item.totalPrice) }}
|
</td>
|
<td class="border border-solid border-[#dcdfe6] p-2">
|
{{ item.remark || '-' }}
|
</td>
|
</tr>
|
<tr v-if="printData.items && printData.items.length > 0">
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center"
|
>
|
合计
|
</td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
></td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
></td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
>
|
{{ formatCount(summaries.count) }}
|
</td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
></td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
>
|
{{ formatPrice(summaries.totalProductPrice) }}
|
</td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
></td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
>
|
{{ formatPrice(summaries.taxPrice) }}
|
</td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
>
|
{{ formatPrice(summaries.totalPrice) }}
|
</td>
|
<td
|
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
></td>
|
</tr>
|
<tr v-if="printRows.length === 0">
|
<td
|
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
colspan="10"
|
>
|
暂无明细
|
</td>
|
</tr>
|
</tbody>
|
</table>
|
|
<!-- 金额信息 -->
|
<div
|
class="mt-3 grid grid-cols-4 gap-x-6 gap-y-2 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]"
|
>
|
<div>优惠率(%):{{ formatPrice(printData.discountPercent) }}</div>
|
<div>付款优惠(元):{{ formatPrice(printData.discountPrice) }}</div>
|
<div>优惠后金额(元):{{ formatPrice(printData.totalPrice) }}</div>
|
<div>收取订金(元):{{ formatPrice(printData.depositPrice) }}</div>
|
</div>
|
|
<!-- 盖章栏 -->
|
<div
|
class="mt-6 grid grid-cols-2 gap-x-10 gap-y-4 border border-dashed border-[#dcdfe6] p-4 text-sm leading-[1.8]"
|
>
|
<div>
|
客户盖章/签字:<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
|
</div>
|
<div>
|
公司盖章/签字:<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
|
</div>
|
<div>
|
日期:<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
|
</div>
|
<div>
|
日期:<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
|
</div>
|
</div>
|
|
<!-- 打印信息 -->
|
<div
|
class="mt-4 flex justify-between text-xs text-[#909399]"
|
>
|
<div>
|
打印人员:{{ userStore.userInfo?.nickname || '-' }}
|
</div>
|
<div>打印时间:{{ printTime || '-' }}</div>
|
</div>
|
</div>
|
</Teleport>
|
</template>
|
|
<style scoped>
|
@page {
|
margin: 8mm 10mm;
|
}
|
|
@media print {
|
:global(body.erp-sale-order-printing) {
|
padding: 0 !important;
|
margin: 0 !important;
|
-webkit-print-color-adjust: exact;
|
print-color-adjust: exact;
|
}
|
|
:global(body.erp-sale-order-printing *) {
|
visibility: hidden !important;
|
}
|
|
:global(body.erp-sale-order-printing .erp-sale-order-print),
|
:global(body.erp-sale-order-printing .erp-sale-order-print *) {
|
visibility: visible !important;
|
}
|
|
:global(body.erp-sale-order-printing .erp-sale-order-print) {
|
position: absolute;
|
top: 0;
|
left: 0;
|
z-index: auto;
|
box-sizing: border-box;
|
width: 100%;
|
padding: 0 !important;
|
margin: 0 !important;
|
pointer-events: auto;
|
opacity: 1;
|
}
|
}
|
</style>
|