feat(pl): 按单据类型限制打印项并调整送(提)货单布局

按入库/出库/直销限制可打印单据;送(提)货单信息区改为一行布局,物料表增加单据编号列,并修正条款列宽与类型多选不换行。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-26 21:05:11 +08:00
1 parent 1e2e8ab0fc
commit 70bdd820ce
4 files changed
+367 -191

No files matched your search

@@ -273,24 +273,38 @@
>
<el-icon><CopyDocument /></el-icon>复制磅单二维码
</el-button>
<el-button
v-if="showInboundSlipPrint"
@click="handlePrintInboundSlip"
<el-dropdown
v-if="showSlipPrint"
trigger="click"
@command="handlePrintSlip"
>
<el-icon><Printer /></el-icon>入库单
</el-button>
<el-button
v-if="showDeliverySlipPrint"
@click="handlePrintDeliverySlip"
>
<el-icon><Printer /></el-icon>送(提)货单
</el-button>
<el-button
v-if="showSubpackSlipPrint"
@click="handlePrintSubpackSlip"
>
<el-icon><Printer /></el-icon>分装单
</el-button>
<el-button>
<el-icon><Printer /></el-icon>打印单据
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="inbound"
:disabled="!canPrintSlipCommand(plan.planType, 'inbound')"
>
入库单
</el-dropdown-item>
<el-dropdown-item
command="delivery"
:disabled="!canPrintSlipCommand(plan.planType, 'delivery')"
>
送(提)货单
</el-dropdown-item>
<el-dropdown-item
command="subpack"
:disabled="!canPrintSlipCommand(plan.planType, 'subpack')"
>
分装单
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template v-else>
<el-button @click="innerVisible = false">
@@ -306,7 +320,7 @@
</template>
<script setup lang="ts">
import { Check, Close, CopyDocument, Printer, View } from '@element-plus/icons-vue'
import { ArrowDown, Check, Close, CopyDocument, Printer, View } from '@element-plus/icons-vue'
import {
carrierApi,
planApi,
@@ -336,7 +350,7 @@ import { useDictStore } from '@/store/modules/dict'
import { isTonProductUnit } from '@/utils/crm-product-display'
import { formatDate } from '@/utils/format'
import { pickBxData, pickBxList } from '@/utils/http/bx'
import { formatPlVehiclePlateDisplay } from '@/utils/pl-display'
import { canPrintSlipCommand, formatPlVehiclePlateDisplay, type PlPrintSlipCommand } from '@/utils/pl-display'
const dictStore = useDictStore()
@@ -427,9 +441,7 @@ const dialogTitle = computed(() => {
const isRearrange = computed(() => plan.value.transportStatus === 'arranged')
/** 安排成功后始终展示打印入口,与运输计划列表工具栏一致,便于现场立即开单 */
const showInboundSlipPrint = computed(() => arrangeSuccess.value)
const showDeliverySlipPrint = computed(() => arrangeSuccess.value)
const showSubpackSlipPrint = computed(() => arrangeSuccess.value)
const showSlipPrint = computed(() => arrangeSuccess.value)
function getUnitLabel(unit: string) {
return dictStore.getLabel('crm_product_unit', unit) || unit
@@ -787,31 +799,28 @@ async function handleSubmit() {
}
}
function handlePrintInboundSlip() {
function handlePrintSlip(command: string) {
const planId = plan.value.id
if (!planId) {
ElMessage.warning('计划信息无效')
return
}
inboundSlipPrintRef.value?.open(planId)
}
function handlePrintDeliverySlip() {
const planId = plan.value.id
if (!planId) {
ElMessage.warning('计划信息无效')
const slipCommand = command as PlPrintSlipCommand
if (!canPrintSlipCommand(plan.value.planType, slipCommand)) {
ElMessage.warning('当前单据类型不支持打印该单据')
return
}
deliverySlipPrintRef.value?.open(planId)
}
function handlePrintSubpackSlip() {
const planId = plan.value.id
if (!planId) {
ElMessage.warning('计划信息无效')
if (slipCommand === 'inbound') {
inboundSlipPrintRef.value?.open(planId)
return
}
subpackSlipPrintRef.value?.open(planId)
if (slipCommand === 'delivery') {
deliverySlipPrintRef.value?.open(planId)
return
}
if (slipCommand === 'subpack') {
subpackSlipPrintRef.value?.open(planId)
}
}
</script>
+211 -81
View File
@@ -29,17 +29,18 @@ const INFO_COLGROUP = `<colgroup>
<col span="7" style="width:14.28%;" />
</colgroup>`
/**
* 物料表列宽:加宽编号/名称,收窄规格/单位;前 5 列合计供条款区使用。
* 物料编号 / 物料名称 / 规格 / 单位 / 数量 / 产品规格 / 实收数量
* 物料表列宽:单据编号 + 加宽编号/名称,收窄规格/单位;前 5 列合计供条款区使用。
* 单据编号 / 物料编号 / 物料名称 / 规格 / 单位 / 数量 / 产品规格 / 实收数量
*/
const PRODUCT_COLGROUP = `<colgroup>
<col style="width:18%;" />
<col style="width:24%;" />
<col style="width:14%;" />
<col style="width:14%;" />
<col style="width:20%;" />
<col style="width:8%;" />
<col style="width:6%;" />
<col style="width:12%;" />
<col style="width:16%;" />
<col style="width:16%;" />
<col style="width:10%;" />
<col style="width:14%;" />
<col style="width:14%;" />
</colgroup>`
/** 物料网格 padding(编辑器预览) */
@@ -87,9 +88,9 @@ const INFO_VALUE_INLINE_LEFT = `flex:1;min-width:0;box-sizing:border-box;border-
const INFO_VALUE_INLINE_CENTER = `flex:1;min-width:0;box-sizing:border-box;border-bottom:${INFO_UNDERLINE} !important;padding:0 2px 2px;text-align:center;min-height:1.3em;`
const INFO_CELL_LEFT = `${NO_BORDER}text-align:left;`
const INFO_CELL_RIGHT = `${NO_BORDER}text-align:left;`
const INFO_GAP_BEFORE_PRODUCT = 24
const INFO_GAP_BEFORE_PRODUCT = 12
const GAP_BEFORE_PRODUCT_CELL = `border:none !important;padding:0 !important;line-height:0 !important;font-size:0 !important;background:transparent !important;height:${INFO_GAP_BEFORE_PRODUCT}px !important;min-height:${INFO_GAP_BEFORE_PRODUCT}px !important;`
const INFO_GAP_BEFORE_SIGN = 24
const INFO_GAP_BEFORE_SIGN = 12
const GAP_BEFORE_SIGN_CELL = `border:none !important;padding:0 !important;line-height:0 !important;font-size:0 !important;background:transparent !important;height:${INFO_GAP_BEFORE_SIGN}px !important;min-height:${INFO_GAP_BEFORE_SIGN}px !important;`
const SIDE_LABELS_COL_WIDTH = 36
@@ -99,7 +100,7 @@ function buildDeliverySlipSignItem(label: string, valueContent: string, minWidth
function buildDeliverySlipSignRowHtml(): string {
return `<tr class="delivery-slip-sign-row">
<td colspan="7" class="delivery-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
<td colspan="8" class="delivery-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
<div class="delivery-slip-sign-inner">
${buildDeliverySlipSignItem('仓库:', '&nbsp;', '80px')}
${buildDeliverySlipSignItem('审核:', '&nbsp;', '80px')}
@@ -165,15 +166,22 @@ function formatDeliverySlipProductGridCellContent(content: string): string {
return plain || '&nbsp;'
}
function buildProductRows(products: PlanProduct[] = [], ctx?: DeliverySlipRenderContext): string {
function buildProductRows(
products: PlanProduct[] = [],
planNo = '',
ctx?: DeliverySlipRenderContext
): string {
if (!products.length) {
return `<tr><td class="delivery-slip-product-cell" colspan="7" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
return `<tr><td class="delivery-slip-product-cell" colspan="8" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
}
const planNoText = formatDeliverySlipProductGridCellContent(planNo)
const dataRows = products
.map((product) => {
const qty = product.outboundQty ?? product.outboundEstimateQty ?? product.quantity ?? ''
return `<tr>
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${planNoText}</td>
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(resolveSlipProductCode(product, ctx))}</td>
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(product.productName || '')}</td>
<td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatDeliverySlipProductGridCellContent(product.model || '')}</td>
@@ -220,13 +228,14 @@ export const DELIVERY_SLIP_PRINT_CSS = `
flex: 1 !important;
min-width: 0 !important;
display: flex !important;
flex-wrap: wrap !important;
flex-wrap: nowrap !important;
justify-content: flex-start !important;
align-items: center !important;
gap: 14px 18px !important;
gap: 10px 12px !important;
box-sizing: border-box !important;
font-size: 20px !important;
line-height: 1.35 !important;
white-space: nowrap !important;
}
.delivery-slip-doc .delivery-slip-type-check-item {
display: inline-flex !important;
@@ -577,17 +586,44 @@ const SIDE_LABEL_CELL = `border:none !important;padding:0 !important;vertical-al
const DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE = `${INFO_ROW}margin-top:-8px;margin-bottom:20px;`
const DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE =
'flex:1;min-width:0;display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:14px 18px;box-sizing:border-box;font-size:20px;line-height:1.35;'
'flex:1;min-width:0;display:flex;flex-wrap:nowrap;justify-content:flex-start;align-items:center;gap:10px 12px;box-sizing:border-box;font-size:20px;line-height:1.35;white-space:nowrap;'
function buildDeliverySlipTypeCheckboxesHtml(): string {
const item = (label: string) =>
`<label class="delivery-slip-type-check-item" contenteditable="false"><input type="checkbox" class="delivery-slip-type-check-input" />${label}</label>`
return `<div class="delivery-slip-info-row delivery-slip-type-check-wrap" style="${DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE}">
<span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">单据编号:</span>
<span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">日&nbsp;期:</span>
<div class="delivery-slip-type-check-row" style="${DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE}">${item('自提')}${item('出库')}${item('直销')}</div>
</div>`
}
function buildDeliverySlipInfoTableHtml(): string {
return `<table class="delivery-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
${INFO_COLGROUP}
<tr>
<td colspan="2" style="${INFO_CELL_LEFT}">
<div class="delivery-slip-info-row" style="${INFO_ROW}">
<span class="delivery-slip-info-label" style="${INFO_LABEL}">收货单位:</span>
<span class="delivery-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receivingUnit}}&nbsp;</span>
</div>
</td>
<td colspan="2" style="${INFO_CELL_LEFT}">
<div class="delivery-slip-info-row" style="${INFO_ROW}">
<span class="delivery-slip-info-label" style="${INFO_LABEL}">收货联系方式:</span>
<span class="delivery-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receiveContact}}&nbsp;</span>
</div>
</td>
<td colspan="3" style="${INFO_CELL_RIGHT}">
${buildDeliverySlipTypeCheckboxesHtml()}
<div class="delivery-slip-info-row" style="${INFO_ROW}">
<span class="delivery-slip-info-label" style="${INFO_LABEL}">日&nbsp;期:</span>
<span class="delivery-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planDateChinese}}&nbsp;</span>
</div>
</td>
</tr>
</table>`
}
function buildDeliverySlipTermsHtml(): string {
return `<div class="delivery-slip-terms-text" style="${TERMS_TEXT_STYLE}">
1.本送货单如购销双方未签订合同,视为有效;<br />
@@ -622,38 +658,7 @@ export function generateDeliverySlipTemplate(_plan?: Plan): string {
<tr>
<td class="delivery-slip-main-col" style="vertical-align:top;padding-right:6px;border:none !important;background:transparent !important;">
<!-- 1. 基本信息(独立表,不可拖宽高) -->
<table class="delivery-slip-info-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
${INFO_COLGROUP}
<tr>
<td colspan="4" style="${INFO_CELL_LEFT}">
<div class="delivery-slip-info-row" style="${INFO_ROW}">
<span class="delivery-slip-info-label" style="${INFO_LABEL}">收货单位:</span>
<span class="delivery-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receivingUnit}}&nbsp;</span>
</div>
</td>
<td colspan="3" style="${INFO_CELL_RIGHT}">
${buildDeliverySlipTypeCheckboxesHtml()}
<div class="delivery-slip-info-row" style="${INFO_ROW}">
<span class="delivery-slip-info-label" style="${INFO_LABEL}">单据编号:</span>
<span class="delivery-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planNo}}&nbsp;</span>
</div>
</td>
</tr>
<tr>
<td colspan="4" style="${INFO_CELL_LEFT}">
<div class="delivery-slip-info-row" style="${INFO_ROW}">
<span class="delivery-slip-info-label" style="${INFO_LABEL}">收货联系方式:</span>
<span class="delivery-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receiveContact}}&nbsp;</span>
</div>
</td>
<td colspan="3" style="${INFO_CELL_RIGHT}">
<div class="delivery-slip-info-row" style="${INFO_ROW}">
<span class="delivery-slip-info-label" style="${INFO_LABEL}">日&nbsp;期:</span>
<span class="delivery-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planDateChinese}}&nbsp;</span>
</div>
</td>
</tr>
</table>
${buildDeliverySlipInfoTableHtml()}
<div class="delivery-slip-section-gap delivery-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</div>
@@ -661,6 +666,7 @@ export function generateDeliverySlipTemplate(_plan?: Plan): string {
<table class="delivery-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
${PRODUCT_COLGROUP}
<tr class="delivery-slip-product-header">
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">物料编号</th>
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">物料名称</th>
<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">规格</th>
@@ -671,7 +677,7 @@ export function generateDeliverySlipTemplate(_plan?: Plan): string {
</tr>
{{productTable}}
<tr>
<td colspan="7" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
<td colspan="8" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
备注:{{remark}}
</td>
</tr>
@@ -679,7 +685,7 @@ export function generateDeliverySlipTemplate(_plan?: Plan): string {
<td colspan="5" class="delivery-slip-product-cell delivery-slip-terms-cell" style="${PRODUCT_TERMS_CELL_LEFT}">
${buildDeliverySlipTermsHtml()}
</td>
<td colspan="2" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
<td colspan="3" class="delivery-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
${SEAL_BLOCK}
</td>
</tr>
@@ -781,7 +787,7 @@ function patchDeliverySlipProductGridCellAttrs(attrs: string): string {
function normalizeDeliverySlipProductGridCell(html: string): string {
if (!html.includes('delivery-slip-doc') || !html.includes('delivery-slip-product-cell')) return html
let result = html.replace(/<tr([^>]*)>\s*(<th[\s\S]*?<\/th>\s*){7}\s*<\/tr>/g, (rowMatch) => {
let result = html.replace(/<tr([^>]*)>\s*(<th[\s\S]*?<\/th>\s*){8}\s*<\/tr>/g, (rowMatch) => {
if (!rowMatch.includes('物料编号') || rowMatch.includes('colspan')) return rowMatch
let row = rowMatch.includes('delivery-slip-product-header')
? rowMatch
@@ -791,7 +797,7 @@ function normalizeDeliverySlipProductGridCell(html: string): string {
})
})
result = result.replace(/<tr([^>]*)>\s*(<td(?![^>]*colspan)[^>]*>[\s\S]*?<\/td>\s*){7}\s*<\/tr>/g, (rowMatch) => {
result = result.replace(/<tr([^>]*)>\s*(<td(?![^>]*colspan)[^>]*>[\s\S]*?<\/td>\s*){8}\s*<\/tr>/g, (rowMatch) => {
if (
rowMatch.includes('备注') ||
rowMatch.includes('1.本送货单') ||
@@ -1120,9 +1126,10 @@ function normalizeDeliverySlipProductTableFooter(html: string): string {
let result = html
result = result.replace(/<td([^>]*colspan="7"[^>]*)>(\s*)备注:/g, (_match, attrs, space) => {
result = result.replace(/<td([^>]*colspan="[78]"[^>]*)>(\s*)备注:/g, (_match, attrs, space) => {
const withSpan = String(attrs).replace(/colspan="[78]"/, 'colspan="8"')
const patched = patchProductFooterCellAttrs(
attrs,
withSpan,
'delivery-slip-product-cell',
PRODUCT_TABLE_CELL_LEFT
)
@@ -1130,9 +1137,9 @@ function normalizeDeliverySlipProductTableFooter(html: string): string {
})
result = result.replace(
/<td([^>]*colspan="[45]"[^>]*)>\s*(<div class="delivery-slip-terms-text"|\s*1\.本送货单)/g,
/<td([^>]*colspan="[456]"[^>]*)>\s*(<div class="delivery-slip-terms-text"|\s*1\.本送货单)/g,
(_match, attrs, contentStart) => {
const withSpan = String(attrs).replace(/colspan="[45]"/, 'colspan="5"')
const withSpan = String(attrs).replace(/colspan="[456]"/, 'colspan="5"')
const patched = patchProductFooterCellAttrs(
withSpan,
'delivery-slip-product-cell delivery-slip-terms-cell',
@@ -1145,7 +1152,7 @@ function normalizeDeliverySlipProductTableFooter(html: string): string {
result = result.replace(
/<td([^>]*colspan="[23]"[^>]*)>\s*<div class="delivery-slip-seal-block"/g,
(_match, attrs) => {
const withSpan = String(attrs).replace(/colspan="[23]"/, 'colspan="2"')
const withSpan = String(attrs).replace(/colspan="[23]"/, 'colspan="3"')
const patched = patchProductFooterCellAttrs(withSpan, 'delivery-slip-product-cell', PRODUCT_TABLE_CELL_LEFT)
return `<td${patched}>\n <div class="delivery-slip-seal-block"`
}
@@ -1176,7 +1183,7 @@ function normalizeDeliverySlipTermsCell(html: string): string {
}
let result = html.replace(
/<td colspan="[45]"([^>]*)>\s*<div class="delivery-slip-terms-text"/g,
/<td colspan="[456]"([^>]*)>\s*<div class="delivery-slip-terms-text"/g,
(match, attrs) => {
if (attrs.includes('delivery-slip-terms-cell') && attrs.includes('border:0.3px solid #000') && /colspan="5"/.test(match)) {
return match
@@ -1227,10 +1234,15 @@ function normalizeDeliverySlipTermsText(html: string): string {
/** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
function refreshDeliverySlipGapBeforeProduct(html: string): string {
return html.replace(
/<tr class="delivery-slip-gap-before-product">[\s\S]*?<\/tr>/,
`<tr class="delivery-slip-gap-before-product">\n <td colspan="7" class="delivery-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</td>\n </tr>`
)
return html
.replace(
/<tr class="delivery-slip-gap-before-product">[\s\S]*?<\/tr>/,
`<tr class="delivery-slip-gap-before-product">\n <td colspan="8" class="delivery-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</td>\n </tr>`
)
.replace(
/<div class="delivery-slip-section-gap delivery-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
`<div class="delivery-slip-section-gap delivery-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</div>`
)
}
/** 在收货联系方式行与物料表之间插入间距行(兼容已保存模板) */
@@ -1243,32 +1255,42 @@ function normalizeDeliverySlipGapBeforeProduct(html: string): string {
return refreshDeliverySlipGapBeforeProduct(html)
}
const gapRow = `<tr class="delivery-slip-gap-before-product"><td colspan="7" class="delivery-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</td></tr>\n `
const gapRow = `<tr class="delivery-slip-gap-before-product"><td colspan="8" class="delivery-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}">&nbsp;</td></tr>\n `
const productHeader = `(?:单据编号|物料编号)`
const withComment = html.replace(
/(<span class="delivery-slip-info-label"[^>]*>日(?:&nbsp;)?期:<\/span>[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr>\s*<th[^>]*>物料编号)/,
new RegExp(
`(收货联系方式:[\\s\\S]*?<\\/tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr[\\s\\S]*?<th[^>]*>${productHeader})`
),
`$1${gapRow}$2`
)
if (withComment !== html) return withComment
const withHeader = html.replace(
/(<span class="delivery-slip-info-label"[^>]*>日(?:&nbsp;)?期:<\/span>[\s\S]*?<\/tr>\s*)(<tr>\s*<th[^>]*>物料编号)/,
new RegExp(`(收货联系方式:[\\s\\S]*?<\\/tr>\\s*)(<tr[\\s\\S]*?<th[^>]*>${productHeader})`),
`$1${gapRow}$2`
)
if (withHeader !== html) return withHeader
return html.replace(
/(收货联系方式:[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr>\s*<th[^>]*>物料编号|<tr>\s*<th[^>]*>物料编号)/,
new RegExp(
`(<span class="delivery-slip-info-label"[^>]*>日(?:&nbsp;)?期:<\\/span>[\\s\\S]*?<\\/tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr[\\s\\S]*?<th[^>]*>${productHeader}|<tr[\\s\\S]*?<th[^>]*>${productHeader})`
),
`$1${gapRow}$2`
)
}
/** 刷新签字区上方间距行 inline 样式 */
function refreshDeliverySlipGapBeforeSign(html: string): string {
return html.replace(
/<tr class="delivery-slip-gap-before-sign">[\s\S]*?<\/tr>/,
`<tr class="delivery-slip-gap-before-sign">\n <td colspan="7" class="delivery-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</td>\n </tr>`
)
return html
.replace(
/<tr class="delivery-slip-gap-before-sign">[\s\S]*?<\/tr>/,
`<tr class="delivery-slip-gap-before-sign">\n <td colspan="8" class="delivery-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</td>\n </tr>`
)
.replace(
/<div class="delivery-slip-section-gap delivery-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
`<div class="delivery-slip-section-gap delivery-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</div>`
)
}
/** 在物料表与签字区之间插入间距行(兼容已保存模板) */
@@ -1279,16 +1301,16 @@ function normalizeDeliverySlipGapBeforeSign(html: string): string {
return refreshDeliverySlipGapBeforeSign(html)
}
const gapRow = `<tr class="delivery-slip-gap-before-sign"><td colspan="7" class="delivery-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</td></tr>\n\n `
const gapRow = `<tr class="delivery-slip-gap-before-sign"><td colspan="8" class="delivery-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}">&nbsp;</td></tr>\n\n `
const withComment = html.replace(
/(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr>\s*<td colspan="7"[^>]*>\s*仓库:)/,
/(<\/tr>\s*)(<!--\s*下:签字区[\s\S]*?<tr>\s*<td colspan="[78]"[^>]*>\s*仓库:)/,
`$1${gapRow}$2`
)
if (withComment !== html) return withComment
return html.replace(
/(delivery-slip-seal-block[\s\S]*?<\/tr>\s*)(<tr>\s*<td colspan="7"[^>]*>\s*仓库:)/,
/(delivery-slip-seal-block[\s\S]*?<\/tr>\s*)(<tr>\s*<td colspan="[78]"[^>]*>\s*仓库:)/,
`$1${gapRow}$2`
)
}
@@ -1323,7 +1345,7 @@ function normalizeDeliverySlipSignRow(html: string): string {
if (!html.includes('delivery-slip-doc') || !html.includes('仓库:')) return html
let result = html.replace(
/<tr([^>]*)>\s*<td colspan="7"([^>]*)>([\s\S]*?仓库:[\s\S]*?司机\/送货人\(附车牌\)[^<]*[\s\S]*?)<\/td>\s*<\/tr>/g,
/<tr([^>]*)>\s*<td colspan="[78]"([^>]*)>([\s\S]*?仓库:[\s\S]*?司机\/送货人\(附车牌\)[^<]*[\s\S]*?)<\/td>\s*<\/tr>/g,
(match, trAttrs, tdAttrs, content) => {
if (!content.includes('delivery-slip-sign-value') || content.includes('delivery-slip-sign-inner')) {
return match
@@ -1355,7 +1377,7 @@ function normalizeDeliverySlipSignRow(html: string): string {
nextTdAttrs = `${nextTdAttrs} class="delivery-slip-sign-row-cell"`
}
return `<tr${trClassAttr}>\n <td colspan="7"${nextTdAttrs}>\n <div class="delivery-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
return `<tr${trClassAttr}>\n <td colspan="8"${nextTdAttrs}>\n <div class="delivery-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
}
)
@@ -1367,6 +1389,89 @@ function normalizeDeliverySlipSignRow(html: string): string {
return normalizeDeliverySlipSignLabelMarkup(result)
}
/** 信息区:收货单位 / 收货联系方式 / 日期 同一行(兼容旧多行布局) */
function normalizeDeliverySlipInfoLayout(html: string): string {
if (!html.includes('delivery-slip-info-table')) return html
const infoMatch = html.match(/<table class="delivery-slip-info-table"[^>]*>[\s\S]*?<\/table>/i)
if (!infoMatch) return html
const infoHtml = infoMatch[0]
const rowCount = (infoHtml.match(/<tr[\s>]/g) || []).length
const hasPlanNoInInfo = infoHtml.includes('单据编号:')
const dateColWideEnough = /<td[^>]*colspan="3"[^>]*>[\s\S]*?日(?:&nbsp;)?期:/.test(infoHtml)
const oneRowThreeFields =
rowCount === 1 &&
infoHtml.includes('收货单位:') &&
infoHtml.includes('收货联系方式:') &&
/日(?:&nbsp;)?期:/.test(infoHtml) &&
!hasPlanNoInInfo &&
dateColWideEnough
if (oneRowThreeFields) return html
return html.replace(infoMatch[0], buildDeliverySlipInfoTableHtml())
}
/** 物料表:表头补「单据编号」列,并把旧 7 列数据行升为 8 列(兼容已保存模板) */
function normalizeDeliverySlipProductPlanNoColumn(html: string): string {
if (!html.includes('delivery-slip-product-table')) return html
let result = html
if (!/>单据编号<\/th>/.test(result)) {
result = result.replace(
/(<tr[^>]*class="[^"]*delivery-slip-product-header[^"]*"[^>]*>\s*)(<th[^>]*>物料编号<\/th>)/,
`$1<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>\n $2`
)
result = result.replace(
/(<tr>\s*)(<th[^>]*>物料编号<\/th>)/,
`$1<th class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>\n $2`
)
}
// 旧 7 列明细行:在首格前插入单据编号占位
result = result.replace(
/<tr([^>]*)>\s*((?:<td(?![^>]*colspan)[^>]*>[\s\S]*?<\/td>\s*){7})<\/tr>/g,
(rowMatch, trAttrs, cells) => {
if (
rowMatch.includes('备注') ||
rowMatch.includes('1.本送货单') ||
rowMatch.includes('仓库:') ||
rowMatch.includes('暂无产品信息') ||
rowMatch.includes('单据编号')
) {
return rowMatch
}
return `<tr${trAttrs}>\n <td class="delivery-slip-product-cell delivery-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">{{planNo}}</td>\n ${cells}</tr>`
}
)
// 仅升级物料表内备注等跨列,条款区保持 5 列、印章区 3 列
result = result.replace(
/(<table class="delivery-slip-product-table"[^>]*>)([\s\S]*?)(<\/table>)/i,
(_m, open, inner, close) => {
let nextInner = String(inner).replace(/colspan="7"/g, 'colspan="8"')
nextInner = nextInner.replace(
/(<td[^>]*colspan=")6("[^>]*>\s*(?:<div class="delivery-slip-terms-text"|[\s]*1\.本送货单))/g,
'$15$2'
)
nextInner = nextInner.replace(
/(<td[^>]*colspan=")2("[^>]*>\s*<div class="delivery-slip-seal-block")/g,
'$13$2'
)
return `${open}${nextInner}${close}`
}
)
result = result.replace(
/(<table class="delivery-slip-sign-table"[^>]*>)([\s\S]*?)(<\/table>)/i,
(_m, open, inner, close) => `${open}${String(inner).replace(/colspan="7"/g, 'colspan="8"')}${close}`
)
return result
}
/**
* 旧版一体 layout-table / 曾拆出的 footer-table → 信息表 + 物料表(含备注条款) + 签字表。
*/
@@ -1398,6 +1503,7 @@ function migrateDeliverySlipSplitTables(html: string): string {
(_full, inner: string) => {
const headerMatch =
inner.match(/<tr[^>]*class="[^"]*delivery-slip-product-header[^"]*"[\s\S]*?<\/tr>/i) ||
inner.match(/<tr>\s*<th[^>]*>单据编号[\s\S]*?<\/tr>/i) ||
inner.match(/<tr>\s*<th[^>]*>物料编号[\s\S]*?<\/tr>/i)
if (!headerMatch) return _full
@@ -1510,6 +1616,8 @@ function normalizeDeliverySlipDocLayout(html: string): string {
if (result.includes('delivery-slip-doc')) {
result = result.replace(/<style type="text\/css">[\s\S]*?<\/style>/, DELIVERY_SLIP_STYLE_BLOCK)
result = normalizeDeliverySlipInfoLayout(result)
result = normalizeDeliverySlipProductPlanNoColumn(result)
result = normalizeDeliverySlipSignLabelMarkup(result)
result = normalizeDeliverySlipProductGridCell(result)
result = normalizeDeliverySlipProductTableFooter(result)
@@ -1529,7 +1637,7 @@ function refreshDeliverySlipTypeCheckRowStyle(html: string): string {
result = result.replace(
/<div class="delivery-slip-type-check-row"[^>]*>([\s\S]*?)<\/div>/,
(_match, inner) =>
`<div class="delivery-slip-info-row delivery-slip-type-check-wrap" style="${DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE}"><span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">单据编号:</span><div class="delivery-slip-type-check-row" style="${DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE}">${inner}</div></div>`
`<div class="delivery-slip-info-row delivery-slip-type-check-wrap" style="${DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE}"><span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">日&nbsp;期:</span><div class="delivery-slip-type-check-row" style="${DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE}">${inner}</div></div>`
)
}
@@ -1537,6 +1645,10 @@ function refreshDeliverySlipTypeCheckRowStyle(html: string): string {
/<div class="delivery-slip-info-row delivery-slip-type-check-wrap"[^>]*>/g,
`<div class="delivery-slip-info-row delivery-slip-type-check-wrap" style="${DELIVERY_SLIP_TYPE_CHECK_WRAP_STYLE}">`
)
result = result.replace(
/<span class="delivery-slip-info-label delivery-slip-type-check-spacer"[^>]*>[\s\S]*?<\/span>/g,
`<span class="delivery-slip-info-label delivery-slip-type-check-spacer" style="${INFO_LABEL}visibility:hidden;">日&nbsp;期:</span>`
)
result = result.replace(
/<div class="delivery-slip-type-check-row"[^>]*>/g,
`<div class="delivery-slip-type-check-row" style="${DELIVERY_SLIP_TYPE_CHECK_INNER_STYLE}">`
@@ -1557,12 +1669,26 @@ function normalizeDeliverySlipTypeCheckRow(html: string): string {
const checkRowHtml = buildDeliverySlipTypeCheckboxesHtml()
const withClass = result.replace(
/(<td colspan="3"[^>]*style="[^"]*"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>单据编号:)/,
/(<td colspan="3"[^>]*style="[^"]*"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?:&nbsp;)?期:)/,
`$1${checkRowHtml}\n $2`
)
if (withClass !== result) {
result = withClass
} else {
result = result.replace(
/(<td colspan="3"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?:&nbsp;)?期:)/,
`$1${checkRowHtml}\n $2`
)
}
// 兼容旧模板:日期在 colspan=2 单元格时仍插多选
if (!result.includes('delivery-slip-type-check-row')) {
result = result.replace(
/(<td colspan="2"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?:&nbsp;)?期:)/,
`$1${checkRowHtml}\n $2`
)
}
// 兼容更旧模板:单据编号位置仍插多选,随后由 infoLayout 迁移
if (!result.includes('delivery-slip-type-check-row')) {
result = result.replace(
/(<td colspan="3"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>单据编号:)/,
`$1${checkRowHtml}\n $2`
@@ -1673,7 +1799,7 @@ export function replaceDeliverySlipPlaceholders(
'{{receiveContact}}': '',
'{{driverInfo}}': '',
'{{planDateChinese}}': formatPlanDateChinese(plan.planDate),
'{{productTable}}': buildProductRows(plan.products, ctx),
'{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx),
'{{remark}}': plan.remark || '',
'{{companyAddress}}': DELIVERY_SLIP_DEFAULTS.companyAddress,
'{{companyPhone}}': DELIVERY_SLIP_DEFAULTS.companyPhone,
@@ -1691,8 +1817,12 @@ export function replaceDeliverySlipPlaceholders(
normalizeDeliverySlipProductTableFooter(
normalizeDeliverySlipProductColWidths(
normalizeDeliverySlipProductGridCell(
normalizeDeliverySlipGapBeforeProduct(
normalizeDeliverySlipTypeCheckRow(normalizeDeliverySlipSideLabelsPlaceholder(htmlContent))
normalizeDeliverySlipProductPlanNoColumn(
normalizeDeliverySlipInfoLayout(
normalizeDeliverySlipGapBeforeProduct(
normalizeDeliverySlipTypeCheckRow(normalizeDeliverySlipSideLabelsPlaceholder(htmlContent))
)
)
)
)
)
@@ -1732,7 +1862,7 @@ export function restoreDeliverySlipPlaceholders(template: string, plan: Plan): s
// 产品明细行还原为占位符(表头「实收数量」之后、页脚「备注」之前;兼容分表与旧一体表)
result = result.replace(
/(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="7"[^>]*>\s*备注:)/,
/(<th[^>]*>实收数量<\/th>\s*<\/tr>\s*)([\s\S]*?)(\s*<tr>\s*<td colspan="[78]"[^>]*>\s*备注:)/,
'$1{{productTable}}$3'
)
+19
View File
@@ -184,6 +184,25 @@ export function getPlPlanTypeLabel(type?: string) {
return type ? map[type] || type : '—'
}
/** 打印单据下拉项:入库单 / 送(提)货单 / 分装单 */
export type PlPrintSlipCommand = 'inbound' | 'delivery' | 'subpack'
/**
* 按运输计划单据类型返回可打印的单据命令。
* - inbound:仅入库单
* - outbound:送(提)货单、分装单
* - direct / 其他:不可打印
*/
export function getPrintableSlipCommands(planType?: string): PlPrintSlipCommand[] {
if (planType === 'inbound') return ['inbound']
if (planType === 'outbound') return ['delivery', 'subpack']
return []
}
export function canPrintSlipCommand(planType: string | undefined, command: PlPrintSlipCommand) {
return getPrintableSlipCommands(planType).includes(command)
}
export type PlPlanTypeTab = 'all' | 'inbound' | 'outbound' | 'direct'
export const PL_PLAN_TYPE_TABS: { label: string; name: PlPlanTypeTab }[] = [
+90 -72
View File
@@ -88,64 +88,70 @@
<el-icon><Plus /></el-icon>新增计划
</el-button>
<el-button
v-if="selectedPlan && canEditPlan(selectedPlan)"
class="btn-ghost"
:disabled="selectedRows.length !== 1"
:disabled="!selectedPlan || !canEditPlan(selectedPlan)"
@click="handleToolbarEdit"
>
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button
v-if="selectedPlan && canArrangeTransport(selectedPlan)"
class="btn-ghost"
:disabled="selectedRows.length !== 1"
:disabled="!selectedPlan || !canArrangeTransport(selectedPlan)"
@click="handleToolbarArrange"
>
<el-icon><Van /></el-icon>{{ arrangeTransportLabel(selectedPlan) }}
<el-icon><Van /></el-icon>
{{ selectedPlan ? arrangeTransportLabel(selectedPlan) : '安排运输' }}
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarPrintInboundSlip"
<el-dropdown
trigger="click"
:disabled="!selectedPlan"
@command="handleToolbarPrintSlip"
>
<el-icon><Printer /></el-icon>入库单
</el-button>
<el-button class="btn-ghost" :disabled="!selectedPlan">
<el-icon><Printer /></el-icon>打印单据
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="inbound"
:disabled="!canPrintSlipCommand(selectedPlan?.planType, 'inbound')"
>
入库单
</el-dropdown-item>
<el-dropdown-item
command="delivery"
:disabled="!canPrintSlipCommand(selectedPlan?.planType, 'delivery')"
>
送(提)货单
</el-dropdown-item>
<el-dropdown-item
command="subpack"
:disabled="!canPrintSlipCommand(selectedPlan?.planType, 'subpack')"
>
分装单
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarPrintDeliverySlip"
>
<el-icon><Printer /></el-icon>送(提)货单
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarPrintSubpackSlip"
>
<el-icon><Printer /></el-icon>分装单
</el-button>
<el-button
v-if="selectedPlan && canVoidPlan(selectedPlan)"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length !== 1"
:disabled="!selectedPlan || !canVoidPlan(selectedPlan)"
:loading="voidLoading"
@click="handleToolbarVoid"
>
<el-icon><CircleClose /></el-icon>作废
</el-button>
<el-button
v-if="selectedPlan && canDeletePlan(selectedPlan)"
class="btn-ghost"
type="danger"
:disabled="
selectedRows.length !== 1 || !canDeleteByCreator(selectedPlan)
"
:title="
selectedRows.length === 1
? getDeletePlanDisabledTitle(selectedPlan)
: ''
!selectedPlan ||
!canDeletePlan(selectedPlan) ||
!canDeleteByCreator(selectedPlan)
"
:title="selectedPlan ? getDeletePlanDisabledTitle(selectedPlan) : ''"
:loading="deleteLoading"
@click="handleToolbarDelete"
>
@@ -338,24 +344,34 @@
>
<el-icon><Van /></el-icon>{{ arrangeTransportLabel(viewingPlan) }}
</el-button>
<el-button
class="btn-ghost"
@click="handlePrintInboundSlip(viewingPlan)"
>
<el-icon><Printer /></el-icon>入库单
</el-button>
<el-button
class="btn-ghost"
@click="handlePrintDeliverySlip(viewingPlan)"
>
<el-icon><Printer /></el-icon>送(提)货单
</el-button>
<el-button
class="btn-ghost"
@click="handlePrintSubpackSlip(viewingPlan)"
>
<el-icon><Printer /></el-icon>分装单
</el-button>
<el-dropdown trigger="click" @command="handleViewPrintSlipCommand">
<el-button class="btn-ghost">
<el-icon><Printer /></el-icon>打印单据
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="inbound"
:disabled="!canPrintSlipCommand(viewingPlan?.planType, 'inbound')"
>
入库单
</el-dropdown-item>
<el-dropdown-item
command="delivery"
:disabled="!canPrintSlipCommand(viewingPlan?.planType, 'delivery')"
>
送(提)货单
</el-dropdown-item>
<el-dropdown-item
command="subpack"
:disabled="!canPrintSlipCommand(viewingPlan?.planType, 'subpack')"
>
分装单
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-if="canVoidPlan(viewingPlan)"
class="btn-ghost"
@@ -393,6 +409,7 @@
<script setup lang="ts">
import {
ArrowDown,
CircleClose,
Delete,
Edit,
@@ -420,10 +437,12 @@ import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { useListDateShortcut } from '@/composables/useListDateShortcut'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import {
canPrintSlipCommand,
formatPlVehiclePlateDisplay,
normalizePlPersonnelSummaryDisplay,
plPlanTypeTabToFilter,
type PlPlanTypeTab
type PlPlanTypeTab,
type PlPrintSlipCommand
} from '@/utils/pl-display'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
const route = useRoute()
@@ -726,31 +745,30 @@ function handleArrange(row: Plan) {
arrangeDialogVisible.value = true
}
function handlePrintInboundSlip(row: Plan) {
inboundSlipPrintRef.value?.open(row.id)
}
function handlePrintDeliverySlip(row: Plan) {
deliverySlipPrintRef.value?.open(row.id)
}
function handlePrintSubpackSlip(row: Plan) {
function handlePrintSlip(command: PlPrintSlipCommand, row: Plan) {
if (!canPrintSlipCommand(row.planType, command)) {
ElMessage.warning('当前单据类型不支持打印该单据')
return
}
if (command === 'inbound') {
inboundSlipPrintRef.value?.open(row.id)
return
}
if (command === 'delivery') {
deliverySlipPrintRef.value?.open(row.id)
return
}
subpackSlipPrintRef.value?.open(row.id)
}
function handleToolbarPrintInboundSlip() {
if (selectedRows.value.length !== 1) return
handlePrintInboundSlip(selectedRows.value[0])
function handleViewPrintSlipCommand(command: string) {
if (!viewingPlan.value) return
handlePrintSlip(command as PlPrintSlipCommand, viewingPlan.value)
}
function handleToolbarPrintDeliverySlip() {
function handleToolbarPrintSlip(command: string) {
if (selectedRows.value.length !== 1) return
handlePrintDeliverySlip(selectedRows.value[0])
}
function handleToolbarPrintSubpackSlip() {
if (selectedRows.value.length !== 1) return
handlePrintSubpackSlip(selectedRows.value[0])
handlePrintSlip(command as PlPrintSlipCommand, selectedRows.value[0])
}
function handleToolbarEdit() {