提交
This commit is contained in:
1 parent
70bdd820ce
commit
bede140d12
988 files changed
+692310
-775910
No files matched your search
@@ -45,6 +45,8 @@ export interface QuoteProductForm {
|
||||
description?: string
|
||||
sort?: number
|
||||
remark?: string
|
||||
isToxic?: number
|
||||
isExplosive?: number
|
||||
}
|
||||
|
||||
export interface QuoteForm {
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface SupplierInfoForm {
|
||||
phoneFax?: string
|
||||
bankName?: string
|
||||
bankAccount?: string
|
||||
allowToxic?: number
|
||||
allowExplosive?: number
|
||||
status?: string
|
||||
remark?: string
|
||||
}
|
||||
@@ -31,6 +33,8 @@ export interface SupplierInfoVO {
|
||||
phoneFax?: string
|
||||
bankName?: string
|
||||
bankAccount?: string
|
||||
allowToxic?: number
|
||||
allowExplosive?: number
|
||||
status?: string
|
||||
remark?: string
|
||||
createTime?: string
|
||||
|
||||
@@ -1437,6 +1437,17 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 合同/报价打印弹窗:标题与编辑提示同一行垂直居中,顶栏更紧凑 */
|
||||
.el-dialog.generate-print-dialog .el-dialog__header {
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.el-dialog.generate-print-dialog .el-dialog__headerbtn {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.el-dialog.dialog-form-layout .el-dialog__headerbtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
<el-button class="btn-ghost" :loading="saveLoading" @click="handleSaveTemplate">
|
||||
<el-icon><DocumentChecked /></el-icon>保存模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="toggleRedSeal">
|
||||
<el-icon>
|
||||
<component :is="showRedSeal ? Hide : Stamp" />
|
||||
</el-icon>
|
||||
{{ showRedSeal ? '隐藏红章' : '显示红章' }}
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handlePrint">
|
||||
<el-icon><Printer /></el-icon>打印
|
||||
</el-button>
|
||||
@@ -45,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, DocumentChecked, Download, Printer, View } from '@element-plus/icons-vue'
|
||||
import { Close, DocumentChecked, Download, Hide, Printer, Stamp, View } from '@element-plus/icons-vue'
|
||||
import html2canvas from 'html2canvas'
|
||||
import jsPDF from 'jspdf'
|
||||
import { fetchProductDetail } from '@/api/crm/product'
|
||||
@@ -53,6 +59,7 @@ import { planApi, type Plan } from '@/api/pl'
|
||||
import TinyMceEditor from '@/components/TinyMceEditor/index.vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import {
|
||||
buildDeliverySlipCompanySealHtml,
|
||||
generateDeliverySlipTemplate,
|
||||
DELIVERY_SLIP_DOC_MAX_WIDTH,
|
||||
DELIVERY_SLIP_PRINT_CSS,
|
||||
@@ -62,11 +69,14 @@ import {
|
||||
setupDeliverySlipEditorTypeCheckboxes,
|
||||
type DeliverySlipRenderContext
|
||||
} from '@/utils/pl-delivery-slip-template'
|
||||
import { injectSlipSealImages, stripSlipSealImages } from '@/utils/pl-slip-red-seal'
|
||||
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
|
||||
|
||||
const visible = ref(false)
|
||||
const slipContent = ref('')
|
||||
const isPreviewMode = ref(false)
|
||||
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
|
||||
const showRedSeal = ref(true)
|
||||
const saveLoading = ref(false)
|
||||
const pdfGenerating = ref(false)
|
||||
const currentPlanId = ref('')
|
||||
@@ -129,6 +139,7 @@ async function buildSlipRenderContext(plan: Plan): Promise<DeliverySlipRenderCon
|
||||
async function open(planId: string) {
|
||||
currentPlanId.value = planId
|
||||
isPreviewMode.value = false
|
||||
showRedSeal.value = true
|
||||
try {
|
||||
ElMessage.info({ message: '正在生成送(提)货单...', duration: 800 })
|
||||
const res = await planApi.get(planId)
|
||||
@@ -162,6 +173,22 @@ function handleClosed() {
|
||||
currentPlanId.value = ''
|
||||
currentPlan.value = null
|
||||
isPreviewMode.value = false
|
||||
showRedSeal.value = true
|
||||
}
|
||||
|
||||
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
|
||||
function toggleRedSeal() {
|
||||
if (showRedSeal.value) {
|
||||
slipContent.value = stripSlipSealImages(slipContent.value)
|
||||
showRedSeal.value = false
|
||||
return
|
||||
}
|
||||
slipContent.value = injectSlipSealImages(
|
||||
slipContent.value,
|
||||
buildDeliverySlipCompanySealHtml(),
|
||||
'delivery-slip-seal-block'
|
||||
)
|
||||
showRedSeal.value = true
|
||||
}
|
||||
|
||||
async function refreshPlan() {
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
<el-button class="btn-ghost" :loading="saveLoading" @click="handleSaveTemplate">
|
||||
<el-icon><DocumentChecked /></el-icon>保存模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="toggleRedSeal">
|
||||
<el-icon>
|
||||
<component :is="showRedSeal ? Hide : Stamp" />
|
||||
</el-icon>
|
||||
{{ showRedSeal ? '隐藏红章' : '显示红章' }}
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handlePrint">
|
||||
<el-icon><Printer /></el-icon>打印
|
||||
</el-button>
|
||||
@@ -44,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, DocumentChecked, Download, Printer, View } from '@element-plus/icons-vue'
|
||||
import { Close, DocumentChecked, Download, Hide, Printer, Stamp, View } from '@element-plus/icons-vue'
|
||||
import html2canvas from 'html2canvas'
|
||||
import jsPDF from 'jspdf'
|
||||
import { fetchProductDetail } from '@/api/crm/product'
|
||||
@@ -52,6 +58,7 @@ import { planApi, type Plan } from '@/api/pl'
|
||||
import TinyMceEditor from '@/components/TinyMceEditor/index.vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import {
|
||||
buildInboundSlipCompanySealHtml,
|
||||
generateInboundSlipTemplate,
|
||||
INBOUND_SLIP_DOC_MAX_WIDTH,
|
||||
INBOUND_SLIP_PRINT_CSS,
|
||||
@@ -60,11 +67,14 @@ import {
|
||||
restoreInboundSlipPlaceholders,
|
||||
type InboundSlipRenderContext
|
||||
} from '@/utils/pl-inbound-slip-template'
|
||||
import { injectSlipSealImages, stripSlipSealImages } from '@/utils/pl-slip-red-seal'
|
||||
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
|
||||
|
||||
const visible = ref(false)
|
||||
const slipContent = ref('')
|
||||
const isPreviewMode = ref(false)
|
||||
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
|
||||
const showRedSeal = ref(true)
|
||||
const saveLoading = ref(false)
|
||||
const pdfGenerating = ref(false)
|
||||
const currentPlanId = ref('')
|
||||
@@ -127,6 +137,7 @@ async function buildSlipRenderContext(plan: Plan): Promise<InboundSlipRenderCont
|
||||
async function open(planId: string) {
|
||||
currentPlanId.value = planId
|
||||
isPreviewMode.value = false
|
||||
showRedSeal.value = true
|
||||
try {
|
||||
ElMessage.info({ message: '正在生成入库单...', duration: 800 })
|
||||
const res = await planApi.get(planId)
|
||||
@@ -160,6 +171,22 @@ function handleClosed() {
|
||||
currentPlanId.value = ''
|
||||
currentPlan.value = null
|
||||
isPreviewMode.value = false
|
||||
showRedSeal.value = true
|
||||
}
|
||||
|
||||
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
|
||||
function toggleRedSeal() {
|
||||
if (showRedSeal.value) {
|
||||
slipContent.value = stripSlipSealImages(slipContent.value)
|
||||
showRedSeal.value = false
|
||||
return
|
||||
}
|
||||
slipContent.value = injectSlipSealImages(
|
||||
slipContent.value,
|
||||
buildInboundSlipCompanySealHtml(),
|
||||
'inbound-slip-seal-block'
|
||||
)
|
||||
showRedSeal.value = true
|
||||
}
|
||||
|
||||
async function refreshPlan() {
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单据类型" prop="planType">
|
||||
<el-select v-model="formData.planType" placeholder="请选择单据类型" style="width: 100%">
|
||||
<el-select v-model="formData.planType" placeholder="请选择单据类型" clearable style="width: 100%">
|
||||
<el-option label="直销单" value="direct" />
|
||||
<el-option label="入库单" value="inbound" />
|
||||
<el-option label="出库单" value="outbound" />
|
||||
@@ -398,8 +398,7 @@ const formData = reactive({
|
||||
|
||||
const basicRules = {
|
||||
planDate: [{ required: true, message: '请选择计划日期', trigger: 'change' }],
|
||||
belongsCompany: [{ required: true, message: '请选择归属公司', trigger: 'change' }],
|
||||
planType: [{ required: true, message: '请选择单据类型', trigger: 'change' }]
|
||||
belongsCompany: [{ required: true, message: '请选择归属公司', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const productsRef = computed(() => formData.products)
|
||||
@@ -561,7 +560,7 @@ function buildPlanCommonPayload(options?: { allowClearTransport?: boolean }): Pl
|
||||
return {
|
||||
planDate: formData.planDate,
|
||||
belongsCompany: formData.belongsCompany,
|
||||
planType: formData.planType,
|
||||
planType: formData.planType || '',
|
||||
contractNo: formData.contractNo,
|
||||
supplierId: formData.supplierId || undefined,
|
||||
supplierName: formData.supplierName || undefined,
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
<el-button class="btn-ghost" :loading="saveLoading" @click="handleSaveTemplate">
|
||||
<el-icon><DocumentChecked /></el-icon>保存模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="toggleRedSeal">
|
||||
<el-icon>
|
||||
<component :is="showRedSeal ? Hide : Stamp" />
|
||||
</el-icon>
|
||||
{{ showRedSeal ? '隐藏红章' : '显示红章' }}
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handlePrint">
|
||||
<el-icon><Printer /></el-icon>打印
|
||||
</el-button>
|
||||
@@ -45,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, DocumentChecked, Download, Printer, View } from '@element-plus/icons-vue'
|
||||
import { Close, DocumentChecked, Download, Hide, Printer, Stamp, View } from '@element-plus/icons-vue'
|
||||
import html2canvas from 'html2canvas'
|
||||
import jsPDF from 'jspdf'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
@@ -53,6 +59,7 @@ import TinyMceEditor from '@/components/TinyMceEditor/index.vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
buildSubpackSlipCompanySealHtml,
|
||||
formatSubpackSlipDateIso,
|
||||
generateSubpackSlipTemplate,
|
||||
SUBPACK_SLIP_DOC_MAX_WIDTH,
|
||||
@@ -63,11 +70,14 @@ import {
|
||||
setupSubpackSlipEditorCheckboxes,
|
||||
type SubpackSlipRenderContext
|
||||
} from '@/utils/pl-subpack-slip-template'
|
||||
import { injectSlipSealImages, stripSlipSealImages } from '@/utils/pl-slip-red-seal'
|
||||
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
|
||||
|
||||
const visible = ref(false)
|
||||
const slipContent = ref('')
|
||||
const isPreviewMode = ref(false)
|
||||
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
|
||||
const showRedSeal = ref(true)
|
||||
const saveLoading = ref(false)
|
||||
const pdfGenerating = ref(false)
|
||||
const currentPlanId = ref('')
|
||||
@@ -120,6 +130,7 @@ async function buildSlipRenderContext(plan: Plan): Promise<SubpackSlipRenderCont
|
||||
async function open(planId: string) {
|
||||
currentPlanId.value = planId
|
||||
isPreviewMode.value = false
|
||||
showRedSeal.value = true
|
||||
try {
|
||||
ElMessage.info({ message: '正在生成分装单...', duration: 800 })
|
||||
const res = await planApi.get(planId)
|
||||
@@ -144,6 +155,8 @@ async function open(planId: string) {
|
||||
|
||||
const renderCtx = await buildSlipRenderContext(plan)
|
||||
slipContent.value = replaceSubpackSlipPlaceholders(htmlContent, plan, renderCtx)
|
||||
// 新模板默认已去掉印章区;若历史模板仍含章,按内容同步按钮态
|
||||
showRedSeal.value = /<img[^>]*alt=["']印章["'][^>]*>/i.test(slipContent.value)
|
||||
visible.value = true
|
||||
} catch (error) {
|
||||
console.error('打开分装单打印失败', error)
|
||||
@@ -157,6 +170,24 @@ function handleClosed() {
|
||||
currentPlan.value = null
|
||||
currentSubpackSlipNo.value = ''
|
||||
isPreviewMode.value = false
|
||||
showRedSeal.value = true
|
||||
}
|
||||
|
||||
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
|
||||
function toggleRedSeal() {
|
||||
if (showRedSeal.value) {
|
||||
slipContent.value = stripSlipSealImages(slipContent.value)
|
||||
showRedSeal.value = false
|
||||
return
|
||||
}
|
||||
const sealHtml = buildSubpackSlipCompanySealHtml()
|
||||
let next = injectSlipSealImages(slipContent.value, sealHtml, 'subpack-slip-seal-block')
|
||||
// 新模板已去掉印章区时,回退到标题区(position:relative)右上角
|
||||
if (!/<img[^>]*alt=["']印章["'][^>]*>/i.test(next) && next.includes('subpack-slip-title-section')) {
|
||||
next = next.replace(/(<div class="subpack-slip-title-section"[^>]*>)/, `$1\n ${sealHtml}\n `)
|
||||
}
|
||||
slipContent.value = next
|
||||
showRedSeal.value = /<img[^>]*alt=["']印章["'][^>]*>/i.test(next)
|
||||
}
|
||||
|
||||
async function refreshPlan() {
|
||||
|
||||
@@ -24,11 +24,25 @@ export interface PlanFormProductRow {
|
||||
legalUseCertUrl?: string
|
||||
}
|
||||
|
||||
export function resolveBelongsCompany(supplierName?: string) {
|
||||
if (supplierName?.includes('安徽云栋')) return 'yundong'
|
||||
/** 从合同交易主体/甲乙方名称解析运输计划归属公司 */
|
||||
export function resolveBelongsCompany(...names: Array<string | undefined | null>) {
|
||||
const text = names.map((n) => (n || '').trim()).filter(Boolean).join(' ')
|
||||
// 与合同模板/报价一致:含「云栋」视为安徽云栋,否则默认安庆山峡
|
||||
if (text.includes('云栋')) return 'yundong'
|
||||
return 'shanxia'
|
||||
}
|
||||
|
||||
/** 优先取交易主体名称;缺失时从甲/乙方中识别我方主体 */
|
||||
export function resolveTradingEntityName(contract: Pick<ContractVO, 'supplierName' | 'partyA' | 'partyB'>) {
|
||||
const supplierName = (contract.supplierName || '').trim()
|
||||
if (supplierName) return supplierName
|
||||
for (const name of [contract.partyA, contract.partyB]) {
|
||||
const n = (name || '').trim()
|
||||
if (n && (n.includes('山峡') || n.includes('云栋'))) return n
|
||||
}
|
||||
return (contract.partyA || '').trim()
|
||||
}
|
||||
|
||||
export function mapContractProductToPlanRow(item: ContractProductForm, index: number): PlanFormProductRow {
|
||||
const productId = String(item.productId || item.id || index)
|
||||
return {
|
||||
@@ -65,7 +79,8 @@ export function applyContractToPlanForm(contract: ContractVO) {
|
||||
return {
|
||||
contractId: String(contract.id),
|
||||
contractNo: contract.contractNo,
|
||||
belongsCompany: resolveBelongsCompany(contract.supplierName || contract.partyA),
|
||||
// 归属公司从合同交易主体带出,表单仍允许用户手动改
|
||||
belongsCompany: resolveBelongsCompany(resolveTradingEntityName(contract)),
|
||||
supplierId: contract.supplierId != null ? String(contract.supplierId) : '',
|
||||
supplierName,
|
||||
destinationId: contract.customerId != null ? String(contract.customerId) : '',
|
||||
|
||||
@@ -484,6 +484,13 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
component: '/crm/customer/pool/index',
|
||||
meta: { title: '公海池', keepAlive: true, menuCode: 'crm:customerPool:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM3000000000000000000000000040',
|
||||
name: 'CrmSupplierHazard',
|
||||
path: 'supplier-hazard',
|
||||
component: '/crm/supplier-hazard/index',
|
||||
meta: { title: '主体危化品能力', keepAlive: true, menuCode: 'crm:supplierHazard:list' }
|
||||
},
|
||||
{
|
||||
id: 'CRM1000000000000000000000000004',
|
||||
name: 'CrmPoolRule',
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/** 主体危化品能力:产品是否允许选择 */
|
||||
|
||||
export function productAllowedByHazard(
|
||||
isToxic: number | undefined,
|
||||
isExplosive: number | undefined,
|
||||
allowToxic: number,
|
||||
allowExplosive: number
|
||||
): boolean {
|
||||
const toxic = Number(isToxic || 0) === 1
|
||||
const explosive = Number(isExplosive || 0) === 1
|
||||
if (!toxic && !explosive) return true
|
||||
if (toxic && explosive) return allowToxic === 1 && allowExplosive === 1
|
||||
if (toxic) return allowToxic === 1
|
||||
return allowExplosive === 1
|
||||
}
|
||||
|
||||
export function removeProductsNotAllowedByHazard<
|
||||
T extends {
|
||||
productId?: string | number
|
||||
productName?: string
|
||||
isToxic?: number
|
||||
isExplosive?: number
|
||||
}
|
||||
>(
|
||||
products: T[],
|
||||
allowToxic: number,
|
||||
allowExplosive: number
|
||||
): { kept: T[]; removedNames: string[] } {
|
||||
const kept: T[] = []
|
||||
const removedNames: string[] = []
|
||||
for (const p of products) {
|
||||
if (productAllowedByHazard(p.isToxic, p.isExplosive, allowToxic, allowExplosive)) {
|
||||
kept.push(p)
|
||||
} else {
|
||||
removedNames.push(p.productName || String(p.productId || ''))
|
||||
}
|
||||
}
|
||||
return { kept, removedNames }
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
|
||||
/** 入库单打印默认公司信息(可在富文本中修改) */
|
||||
export const INBOUND_SLIP_DEFAULTS = {
|
||||
companyName: '安庆市山峡化工有限公司',
|
||||
receivingUnit: '安庆市山峡化工有限公司',
|
||||
companyAddress: '安庆市高新区霞虹路5号',
|
||||
companyPhone: '0556-5999710',
|
||||
contactPhone: '方总13395560066'
|
||||
} as const
|
||||
|
||||
/** 旧模板曾把送货单位写死为本公司名,保存还原时一并识别 */
|
||||
const LEGACY_RECEIVING_UNIT = '安庆市山峡化工有限公司'
|
||||
|
||||
const DOC_FONT = "SimSun, 'Songti SC', serif"
|
||||
const FONT_TITLE = 'font-size:26px;font-weight:bold;'
|
||||
const FONT_BODY = 'font-size:20px;'
|
||||
@@ -29,17 +31,18 @@ const INFO_COLGROUP = `<colgroup>
|
||||
<col span="7" style="width:14.28%;" />
|
||||
</colgroup>`
|
||||
/**
|
||||
* 物料表列宽:加宽编号/名称,收窄规格/单位;前 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(编辑器预览) */
|
||||
@@ -62,16 +65,12 @@ const PRODUCT_GRID_CELL = `padding:${PRODUCT_GRID_CELL_PADDING};box-sizing:borde
|
||||
const PRODUCT_CELL = `padding:4px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:center;${FONT_BODY}line-height:1.35;background-color:#fff !important;${PRODUCT_CELL_WRAP}`
|
||||
const PRODUCT_TH = `padding:${PRODUCT_GRID_TH_PADDING};box-sizing:border-box;border:${PRODUCT_GRID_BORDER} !important;text-align:center;${FONT_BODY}line-height:normal;font-weight:bold;background-color:#fff !important;${PRODUCT_CELL_WRAP}`
|
||||
const PRODUCT_TABLE_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:left;${FONT_BODY}line-height:1.35;vertical-align:top;background-color:#fff !important;`
|
||||
/** 条款单元格:行高随内容,避免被右侧印章区 min-height 撑出底部空白 */
|
||||
/** 条款单元格样式(兼容已保存模板归一化) */
|
||||
const PRODUCT_TERMS_CELL_LEFT = `padding:10px 6px 14px 6px;border:${PRODUCT_TABLE_BORDER} !important;text-align:left;${FONT_BODY}line-height:1.35;vertical-align:top;background-color:#fff !important;`
|
||||
const TERMS_TEXT_STYLE = 'line-height:1.35;'
|
||||
const SEAL_BLOCK_LINE_STYLE = 'line-height:1.35;'
|
||||
/** 入库单印章:public/sxshzyz.png,定位在地址单元格右上角(相对原位置向上 66px) */
|
||||
/** 入库单印章:public/sxshzyz.png(兼容已保存模板的红章开关) */
|
||||
const INBOUND_SLIP_SEAL_IMAGE_SRC = '/sxshzyz.png'
|
||||
const INBOUND_SLIP_SEAL_TOP = -66
|
||||
const INBOUND_SLIP_SEAL_IMAGE_STYLE = `position:absolute;right:4px;top:${INBOUND_SLIP_SEAL_TOP}px;width:140px;height:140px;opacity:0.85;z-index:10;`
|
||||
const HANDWRITE_BLANK_INNER = ' '
|
||||
const HANDWRITE_BLANK = `<span class="inbound-slip-handwrite-blank">(${HANDWRITE_BLANK_INNER})</span>`
|
||||
const PLAIN_CELL = `border:none !important;padding:${TABLE_ROW_PADDING};${FONT_BODY}line-height:1.8;vertical-align:top;background:transparent !important;`
|
||||
const NO_BORDER = `${PLAIN_CELL} vertical-align:bottom;`
|
||||
|
||||
@@ -87,9 +86,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 +98,7 @@ function buildInboundSlipSignItem(label: string, valueContent: string, minWidth:
|
||||
|
||||
function buildInboundSlipSignRowHtml(): string {
|
||||
return `<tr class="inbound-slip-sign-row">
|
||||
<td colspan="7" class="inbound-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
|
||||
<td colspan="8" class="inbound-slip-sign-row-cell" style="border:none !important;padding:${TABLE_ROW_PADDING};text-align:center;${FONT_SIGN}background:transparent !important;">
|
||||
<div class="inbound-slip-sign-inner">
|
||||
${buildInboundSlipSignItem('仓库:', ' ', '80px')}
|
||||
${buildInboundSlipSignItem('审核:', ' ', '80px')}
|
||||
@@ -149,6 +148,10 @@ function formatQty(value?: number | string | null): string {
|
||||
return num.toFixed(3)
|
||||
}
|
||||
|
||||
function buildDeliveryUnit(plan: Plan): string {
|
||||
return plan.customerName?.trim() || plan.destinationName?.trim() || ''
|
||||
}
|
||||
|
||||
function formatInboundSlipProductGridCellContent(content: string): string {
|
||||
const plain = content
|
||||
.trim()
|
||||
@@ -161,15 +164,22 @@ function formatInboundSlipProductGridCellContent(content: string): string {
|
||||
return plain || ' '
|
||||
}
|
||||
|
||||
function buildProductRows(products: PlanProduct[] = [], ctx?: InboundSlipRenderContext): string {
|
||||
function buildProductRows(
|
||||
products: PlanProduct[] = [],
|
||||
planNo = '',
|
||||
ctx?: InboundSlipRenderContext
|
||||
): string {
|
||||
if (!products.length) {
|
||||
return `<tr><td class="inbound-slip-product-cell" colspan="7" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
|
||||
return `<tr><td class="inbound-slip-product-cell" colspan="8" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
|
||||
}
|
||||
|
||||
const planNoText = formatInboundSlipProductGridCellContent(planNo)
|
||||
|
||||
const dataRows = products
|
||||
.map((product) => {
|
||||
const qty = product.inboundQty ?? product.inboundEstimateQty ?? product.quantity ?? ''
|
||||
return `<tr>
|
||||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${planNoText}</td>
|
||||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(resolveSlipProductCode(product, ctx))}</td>
|
||||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(product.productName || '')}</td>
|
||||
<td class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">${formatInboundSlipProductGridCellContent(product.model || '')}</td>
|
||||
@@ -451,25 +461,8 @@ export function buildInboundSlipCompanySealHtml(): string {
|
||||
return `<img src="${INBOUND_SLIP_SEAL_IMAGE_SRC}" alt="印章" style="${INBOUND_SLIP_SEAL_IMAGE_STYLE}" />`
|
||||
}
|
||||
|
||||
const SEAL_BLOCK = `<div class="inbound-slip-seal-block" style="position:relative;padding:0;${FONT_BODY}${SEAL_BLOCK_LINE_STYLE}">
|
||||
<!-- 印章图片 -->
|
||||
{{companySeal}}
|
||||
<div class="inbound-slip-seal-lines" style="position:relative;z-index:1;${SEAL_BLOCK_LINE_STYLE}margin:0;padding:0;">
|
||||
地址:{{companyAddress}}<br />
|
||||
送货单位固话:{{companyPhone}}<br />
|
||||
联系人电话:{{contactPhone}}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
const SIDE_LABEL_CELL = `border:none !important;padding:0 !important;vertical-align:top;text-align:center;${FONT_SIDE}line-height:1.35;background:transparent !important;`
|
||||
|
||||
function buildInboundSlipTermsHtml(): string {
|
||||
return `<div class="inbound-slip-terms-text" style="${TERMS_TEXT_STYLE}">
|
||||
1.本送货单如购销双方未签订合同,视为有效;<br />
|
||||
2.本次送货含桶${HANDWRITE_BLANK}个,回收${HANDWRITE_BLANK}个,结存${HANDWRITE_BLANK}个;
|
||||
</div>`
|
||||
}
|
||||
|
||||
/** 右侧联次:单列纵向,带圈序号 + 每字一行依次向下排列 */
|
||||
export function buildInboundSlipSideLabelsHtml(): string {
|
||||
return `<table class="inbound-slip-side-labels" style="border:none !important;border-collapse:collapse;margin:0 auto;width:auto;">
|
||||
@@ -502,24 +495,10 @@ export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||||
<tr>
|
||||
<td colspan="4" style="${INFO_CELL_LEFT}">
|
||||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">收货单位:</span>
|
||||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">送货单位:</span>
|
||||
<span class="inbound-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receivingUnit}} </span>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="3" style="${INFO_CELL_RIGHT}">
|
||||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">单据编号:</span>
|
||||
<span class="inbound-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planNo}} </span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" style="${INFO_CELL_LEFT}">
|
||||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">收货联系方式:</span>
|
||||
<span class="inbound-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receiveContact}} </span>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="3" style="${INFO_CELL_RIGHT}">
|
||||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">日 期:</span>
|
||||
@@ -531,10 +510,11 @@ export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||||
|
||||
<div class="inbound-slip-section-gap inbound-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>
|
||||
|
||||
<!-- 2. 物料明细表(含备注、条款+印章;可拖表头/明细列宽行高) -->
|
||||
<!-- 2. 物料明细表(含备注;可拖表头/明细列宽行高) -->
|
||||
<table class="inbound-slip-product-table" style="width:100%;border-collapse:collapse;table-layout:fixed;border:none !important;">
|
||||
${PRODUCT_COLGROUP}
|
||||
<tr class="inbound-slip-product-header">
|
||||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>
|
||||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">物料编号</th>
|
||||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">物料名称</th>
|
||||
<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">规格</th>
|
||||
@@ -545,18 +525,10 @@ export function generateInboundSlipTemplate(_plan?: Plan): string {
|
||||
</tr>
|
||||
{{productTable}}
|
||||
<tr>
|
||||
<td colspan="7" class="inbound-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
<td colspan="8" class="inbound-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
备注:{{remark}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" class="inbound-slip-product-cell inbound-slip-terms-cell" style="${PRODUCT_TERMS_CELL_LEFT}">
|
||||
${buildInboundSlipTermsHtml()}
|
||||
</td>
|
||||
<td colspan="2" class="inbound-slip-product-cell" style="${PRODUCT_TABLE_CELL_LEFT}">
|
||||
${SEAL_BLOCK}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="inbound-slip-section-gap inbound-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>
|
||||
@@ -655,7 +627,7 @@ function patchInboundSlipProductGridCellAttrs(attrs: string): string {
|
||||
function normalizeInboundSlipProductGridCell(html: string): string {
|
||||
if (!html.includes('inbound-slip-doc') || !html.includes('inbound-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('inbound-slip-product-header')
|
||||
? rowMatch
|
||||
@@ -665,7 +637,7 @@ function normalizeInboundSlipProductGridCell(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.本送货单') ||
|
||||
@@ -862,9 +834,10 @@ function normalizeInboundSlipProductTableFooter(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,
|
||||
'inbound-slip-product-cell',
|
||||
PRODUCT_TABLE_CELL_LEFT
|
||||
)
|
||||
@@ -949,33 +922,44 @@ function normalizeInboundSlipProductColWidths(html: string): string {
|
||||
)
|
||||
}
|
||||
|
||||
/** 条款区两行间距与括号内手写空白(兼容已保存模板) */
|
||||
/** 移除条款+地址印章行(兼容已保存模板) */
|
||||
function normalizeInboundSlipTermsText(html: string): string {
|
||||
if (!html.includes('inbound-slip-doc') || !html.includes('1.本送货单')) {
|
||||
if (!html.includes('inbound-slip-doc')) return html
|
||||
if (
|
||||
!html.includes('1.本送货单') &&
|
||||
!html.includes('inbound-slip-terms-text') &&
|
||||
!html.includes('inbound-slip-seal-block')
|
||||
) {
|
||||
return html
|
||||
}
|
||||
|
||||
const termsHtml = buildInboundSlipTermsHtml()
|
||||
|
||||
if (html.includes('inbound-slip-terms-text')) {
|
||||
return html.replace(/<div class="inbound-slip-terms-text"[\s\S]*?<\/div>/, termsHtml.trim())
|
||||
}
|
||||
|
||||
return html.replace(
|
||||
/1\.本送货单如购销双方未签订合同,视为有效;[\s\S]*?结存\([^)]*\)个;/,
|
||||
termsHtml.trim()
|
||||
// 条款+印章整行删除(同一 tr 内左右两格)
|
||||
let result = html.replace(
|
||||
/<tr[^>]*>\s*<td[^>]*colspan="[45]"[^>]*>[\s\S]*?(?:inbound-slip-terms-text|1\.本送货单)[\s\S]*?<\/tr>\s*/gi,
|
||||
''
|
||||
)
|
||||
// 兜底:独立印章行
|
||||
result = result.replace(
|
||||
/<tr[^>]*>\s*<td[^>]*>[\s\S]*?inbound-slip-seal-block[\s\S]*?<\/tr>\s*/gi,
|
||||
''
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
/** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
|
||||
function refreshInboundSlipGapBeforeProduct(html: string): string {
|
||||
return html.replace(
|
||||
/<tr class="inbound-slip-gap-before-product">[\s\S]*?<\/tr>/,
|
||||
`<tr class="inbound-slip-gap-before-product">\n <td colspan="7" class="inbound-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
return html
|
||||
.replace(
|
||||
/<tr class="inbound-slip-gap-before-product">[\s\S]*?<\/tr>/,
|
||||
`<tr class="inbound-slip-gap-before-product">\n <td colspan="8" class="inbound-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
.replace(
|
||||
/<div class="inbound-slip-section-gap inbound-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
|
||||
`<div class="inbound-slip-section-gap inbound-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>`
|
||||
)
|
||||
}
|
||||
|
||||
/** 在收货联系方式行与物料表之间插入间距行(兼容已保存模板) */
|
||||
/** 在信息区与物料表之间插入间距(兼容已保存模板) */
|
||||
function normalizeInboundSlipGapBeforeProduct(html: string): string {
|
||||
if (!html.includes('inbound-slip-doc')) {
|
||||
return html
|
||||
@@ -985,16 +969,21 @@ function normalizeInboundSlipGapBeforeProduct(html: string): string {
|
||||
return refreshInboundSlipGapBeforeProduct(html)
|
||||
}
|
||||
|
||||
const gapRow = `<tr class="inbound-slip-gap-before-product"><td colspan="7" class="inbound-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td></tr>\n `
|
||||
const gapRow = `<tr class="inbound-slip-gap-before-product"><td colspan="8" class="inbound-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td></tr>\n `
|
||||
const productHeader = `(?:单据编号|物料编号)`
|
||||
|
||||
const withComment = html.replace(
|
||||
/(<span class="inbound-slip-info-label"[^>]*>日(?: )?期:<\/span>[\s\S]*?<\/tr>\s*)(<!--\s*中:产品明细表[\s\S]*?<tr>\s*<th[^>]*>物料编号)/,
|
||||
new RegExp(
|
||||
`(<span class="inbound-slip-info-label"[^>]*>日(?: )?期:</span>[\\s\\S]*?</tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr>\\s*<th[^>]*>${productHeader})`
|
||||
),
|
||||
`$1${gapRow}$2`
|
||||
)
|
||||
if (withComment !== html) return withComment
|
||||
|
||||
const withHeader = html.replace(
|
||||
/(<span class="inbound-slip-info-label"[^>]*>日(?: )?期:<\/span>[\s\S]*?<\/tr>\s*)(<tr>\s*<th[^>]*>物料编号)/,
|
||||
new RegExp(
|
||||
`(<span class="inbound-slip-info-label"[^>]*>日(?: )?期:</span>[\\s\\S]*?</tr>\\s*)(<tr>\\s*<th[^>]*>${productHeader})`
|
||||
),
|
||||
`$1${gapRow}$2`
|
||||
)
|
||||
if (withHeader !== html) return withHeader
|
||||
@@ -1007,10 +996,15 @@ function normalizeInboundSlipGapBeforeProduct(html: string): string {
|
||||
|
||||
/** 刷新签字区上方间距行 inline 样式 */
|
||||
function refreshInboundSlipGapBeforeSign(html: string): string {
|
||||
return html.replace(
|
||||
/<tr class="inbound-slip-gap-before-sign">[\s\S]*?<\/tr>/,
|
||||
`<tr class="inbound-slip-gap-before-sign">\n <td colspan="7" class="inbound-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
return html
|
||||
.replace(
|
||||
/<tr class="inbound-slip-gap-before-sign">[\s\S]*?<\/tr>/,
|
||||
`<tr class="inbound-slip-gap-before-sign">\n <td colspan="8" class="inbound-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
.replace(
|
||||
/<div class="inbound-slip-section-gap inbound-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
|
||||
`<div class="inbound-slip-section-gap inbound-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>`
|
||||
)
|
||||
}
|
||||
|
||||
/** 在物料表与签字区之间插入间距行(兼容已保存模板) */
|
||||
@@ -1021,16 +1015,16 @@ function normalizeInboundSlipGapBeforeSign(html: string): string {
|
||||
return refreshInboundSlipGapBeforeSign(html)
|
||||
}
|
||||
|
||||
const gapRow = `<tr class="inbound-slip-gap-before-sign"><td colspan="7" class="inbound-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td></tr>\n\n `
|
||||
const gapRow = `<tr class="inbound-slip-gap-before-sign"><td colspan="8" class="inbound-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </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(
|
||||
/(inbound-slip-seal-block[\s\S]*?<\/tr>\s*)(<tr>\s*<td colspan="7"[^>]*>\s*仓库:)/,
|
||||
/(inbound-slip-seal-block[\s\S]*?<\/tr>\s*)(<tr>\s*<td colspan="[78]"[^>]*>\s*仓库:)/,
|
||||
`$1${gapRow}$2`
|
||||
)
|
||||
}
|
||||
@@ -1065,7 +1059,7 @@ function normalizeInboundSlipSignRow(html: string): string {
|
||||
if (!html.includes('inbound-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('inbound-slip-sign-value') || content.includes('inbound-slip-sign-inner')) {
|
||||
return match
|
||||
@@ -1097,7 +1091,7 @@ function normalizeInboundSlipSignRow(html: string): string {
|
||||
nextTdAttrs = `${nextTdAttrs} class="inbound-slip-sign-row-cell"`
|
||||
}
|
||||
|
||||
return `<tr${trClassAttr}>\n <td colspan="7"${nextTdAttrs}>\n <div class="inbound-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
|
||||
return `<tr${trClassAttr}>\n <td colspan="8"${nextTdAttrs}>\n <div class="inbound-slip-sign-inner">${wrappedContent.trim()}</div>\n </td>\n </tr>`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1109,6 +1103,82 @@ function normalizeInboundSlipSignRow(html: string): string {
|
||||
return normalizeInboundSlipSignLabelMarkup(result)
|
||||
}
|
||||
|
||||
/** 信息区:送货单位 + 日期(兼容旧「收货单位/单据编号/收货联系方式」布局) */
|
||||
function normalizeInboundSlipInfoLayout(html: string): string {
|
||||
if (!html.includes('inbound-slip-info-table')) return html
|
||||
|
||||
const infoMatch = html.match(/<table class="inbound-slip-info-table"[^>]*>[\s\S]*?<\/table>/i)
|
||||
if (!infoMatch) return html
|
||||
|
||||
const infoHtml = infoMatch[0]
|
||||
const needsMigrate =
|
||||
infoHtml.includes('收货单位:') ||
|
||||
infoHtml.includes('收货联系方式:') ||
|
||||
infoHtml.includes('单据编号:') ||
|
||||
!infoHtml.includes('送货单位:') ||
|
||||
(infoHtml.match(/<tr[\s>]/g) || []).length > 1
|
||||
|
||||
if (!needsMigrate) return html
|
||||
|
||||
const newInfoTable = `<table class="inbound-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="inbound-slip-info-row" style="${INFO_ROW}">
|
||||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">送货单位:</span>
|
||||
<span class="inbound-slip-info-value-left" style="${INFO_VALUE_INLINE_LEFT}">{{receivingUnit}} </span>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="3" style="${INFO_CELL_RIGHT}">
|
||||
<div class="inbound-slip-info-row" style="${INFO_ROW}">
|
||||
<span class="inbound-slip-info-label" style="${INFO_LABEL}">日 期:</span>
|
||||
<span class="inbound-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planDateChinese}} </span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>`
|
||||
|
||||
return html.replace(infoMatch[0], newInfoTable)
|
||||
}
|
||||
|
||||
/** 物料表:表头补「单据编号」列,并把旧 7 列数据行升为 8 列(兼容已保存模板) */
|
||||
function normalizeInboundSlipProductPlanNoColumn(html: string): string {
|
||||
if (!html.includes('inbound-slip-product-table')) return html
|
||||
|
||||
let result = html
|
||||
|
||||
if (!/>单据编号<\/th>/.test(result)) {
|
||||
result = result.replace(
|
||||
/(<tr[^>]*class="[^"]*inbound-slip-product-header[^"]*"[^>]*>\s*)(<th[^>]*>物料编号<\/th>)/,
|
||||
`$1<th class="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_TH}">单据编号</th>\n $2`
|
||||
)
|
||||
result = result.replace(
|
||||
/(<tr>\s*)(<th[^>]*>物料编号<\/th>)/,
|
||||
`$1<th class="inbound-slip-product-cell inbound-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="inbound-slip-product-cell inbound-slip-product-grid-cell" style="${PRODUCT_GRID_CELL}">{{planNo}}</td>\n ${cells}</tr>`
|
||||
}
|
||||
)
|
||||
|
||||
result = result.replace(/colspan="7"/g, 'colspan="8"')
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧版一体 layout-table / 曾拆出的 footer-table → 方案A:
|
||||
* 信息表 + 物料表(含备注条款) + 签字表。
|
||||
@@ -1143,6 +1213,7 @@ function migrateInboundSlipSplitTables(html: string): string {
|
||||
(_full, inner: string) => {
|
||||
const headerMatch =
|
||||
inner.match(/<tr[^>]*class="[^"]*inbound-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
|
||||
|
||||
@@ -1287,12 +1358,12 @@ export function replaceInboundSlipPlaceholders(
|
||||
|
||||
const replacements: Record<string, string> = {
|
||||
'{{companyName}}': INBOUND_SLIP_DEFAULTS.companyName,
|
||||
'{{receivingUnit}}': INBOUND_SLIP_DEFAULTS.receivingUnit,
|
||||
'{{receivingUnit}}': buildDeliveryUnit(plan),
|
||||
'{{planNo}}': plan.planNo || '',
|
||||
'{{receiveContact}}': '',
|
||||
'{{driverInfo}}': '',
|
||||
'{{planDateChinese}}': formatPlanDateChinese(plan.planDate),
|
||||
'{{productTable}}': buildProductRows(plan.products, ctx),
|
||||
'{{productTable}}': buildProductRows(plan.products, plan.planNo || '', ctx),
|
||||
'{{remark}}': plan.remark || '',
|
||||
'{{companyAddress}}': INBOUND_SLIP_DEFAULTS.companyAddress,
|
||||
'{{companyPhone}}': INBOUND_SLIP_DEFAULTS.companyPhone,
|
||||
@@ -1310,7 +1381,13 @@ export function replaceInboundSlipPlaceholders(
|
||||
normalizeInboundSlipProductTableFooter(
|
||||
normalizeInboundSlipProductColWidths(
|
||||
normalizeInboundSlipProductGridCell(
|
||||
normalizeInboundSlipGapBeforeProduct(normalizeInboundSlipSideLabelsPlaceholder(htmlContent))
|
||||
normalizeInboundSlipProductPlanNoColumn(
|
||||
normalizeInboundSlipInfoLayout(
|
||||
normalizeInboundSlipGapBeforeProduct(
|
||||
normalizeInboundSlipSideLabelsPlaceholder(htmlContent)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1330,9 +1407,10 @@ export function replaceInboundSlipPlaceholders(
|
||||
export function restoreInboundSlipPlaceholders(template: string, plan: Plan): string {
|
||||
let result = template
|
||||
|
||||
const deliveryUnit = buildDeliveryUnit(plan)
|
||||
const dynamicReplacements: Record<string, string> = {
|
||||
'{{companyName}}': INBOUND_SLIP_DEFAULTS.companyName,
|
||||
'{{receivingUnit}}': INBOUND_SLIP_DEFAULTS.receivingUnit,
|
||||
'{{receivingUnit}}': deliveryUnit,
|
||||
'{{planNo}}': plan.planNo || '',
|
||||
'{{planDateChinese}}': formatPlanDateChinese(plan.planDate),
|
||||
'{{remark}}': plan.remark || '',
|
||||
@@ -1347,9 +1425,14 @@ export function restoreInboundSlipPlaceholders(template: string, plan: Plan): st
|
||||
result = result.replace(new RegExp(escaped, 'g'), placeholder)
|
||||
}
|
||||
|
||||
// 旧模板写死的本公司名 → 送货单位占位符
|
||||
if (deliveryUnit !== LEGACY_RECEIVING_UNIT) {
|
||||
result = result.replace(new RegExp(LEGACY_RECEIVING_UNIT.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '{{receivingUnit}}')
|
||||
}
|
||||
|
||||
// 产品明细行还原为占位符(表头「实收数量」与「备注」行之间)
|
||||
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'
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/** 运输单据打印弹窗:红章显隐(本次弹窗有效) */
|
||||
|
||||
export const SLIP_SEAL_IMG_RE = /<img[^>]*alt=["']印章["'][^>]*>/gi
|
||||
|
||||
export function stripSlipSealImages(html: string): string {
|
||||
return html.replace(SLIP_SEAL_IMG_RE, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 将公司章插回单据 HTML。
|
||||
* 优先落在「印章图片」注释后;否则插入指定 seal-block 容器开头。
|
||||
*/
|
||||
export function injectSlipSealImages(
|
||||
html: string,
|
||||
sealHtml: string,
|
||||
sealBlockClass: string
|
||||
): string {
|
||||
let result = stripSlipSealImages(html)
|
||||
if (!sealHtml) return result
|
||||
|
||||
if (/<!--\s*印章图片[^>]*-->/.test(result)) {
|
||||
let sealCommentIndex = 0
|
||||
return result.replace(/<!--\s*印章图片[^>]*-->/g, (match) => {
|
||||
sealCommentIndex += 1
|
||||
if (sealCommentIndex === 1) return `${match}\n ${sealHtml}`
|
||||
return match
|
||||
})
|
||||
}
|
||||
|
||||
if (sealBlockClass && result.includes(sealBlockClass)) {
|
||||
const escaped = sealBlockClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
return result.replace(
|
||||
new RegExp(`(<div class="${escaped}"[^>]*>)`),
|
||||
`$1\n <!-- 印章图片 -->\n ${sealHtml}\n `
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -87,9 +87,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
|
||||
|
||||
@@ -1434,10 +1434,15 @@ function normalizeSubpackSlipRemoveTermsRow(html: string): string {
|
||||
|
||||
/** 刷新已有间距行的 inline 样式(TinyMCE 保存后可能丢失) */
|
||||
function refreshSubpackSlipGapBeforeProduct(html: string): string {
|
||||
return html.replace(
|
||||
/<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/,
|
||||
`<tr class="subpack-slip-gap-before-product">\n <td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
return html
|
||||
.replace(
|
||||
/<tr class="subpack-slip-gap-before-product">[\s\S]*?<\/tr>/,
|
||||
`<tr class="subpack-slip-gap-before-product">\n <td colspan="7" class="subpack-slip-gap-before-product-cell" style="${GAP_BEFORE_PRODUCT_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
.replace(
|
||||
/<div class="subpack-slip-section-gap subpack-slip-gap-before-product"[^>]*>[\s\S]*?<\/div>/,
|
||||
`<div class="subpack-slip-section-gap subpack-slip-gap-before-product" style="${GAP_BEFORE_PRODUCT_CELL}"> </div>`
|
||||
)
|
||||
}
|
||||
|
||||
/** 在收货联系方式行与物料表之间插入间距行(兼容已保存模板) */
|
||||
@@ -1472,10 +1477,15 @@ function normalizeSubpackSlipGapBeforeProduct(html: string): string {
|
||||
|
||||
/** 刷新签字区上方间距行 inline 样式 */
|
||||
function refreshSubpackSlipGapBeforeSign(html: string): string {
|
||||
return html.replace(
|
||||
/<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/,
|
||||
`<tr class="subpack-slip-gap-before-sign">\n <td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
return html
|
||||
.replace(
|
||||
/<tr class="subpack-slip-gap-before-sign">[\s\S]*?<\/tr>/,
|
||||
`<tr class="subpack-slip-gap-before-sign">\n <td colspan="7" class="subpack-slip-gap-before-sign-cell" style="${GAP_BEFORE_SIGN_CELL}"> </td>\n </tr>`
|
||||
)
|
||||
.replace(
|
||||
/<div class="subpack-slip-section-gap subpack-slip-gap-before-sign"[^>]*>[\s\S]*?<\/div>/,
|
||||
`<div class="subpack-slip-section-gap subpack-slip-gap-before-sign" style="${GAP_BEFORE_SIGN_CELL}"> </div>`
|
||||
)
|
||||
}
|
||||
|
||||
/** 在物料表与签字区之间插入间距行(兼容已保存模板) */
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedRows.length === 1 && selectedRows[0]?.level === 1"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || selectedRows[0]?.level !== 1"
|
||||
:loading="loadingMap.rowAddChild"
|
||||
@click="handleToolbarAddChild"
|
||||
>
|
||||
|
||||
@@ -773,18 +773,22 @@
|
||||
</el-dialog>
|
||||
<!-- 生成合同对话框 -->
|
||||
<el-dialog
|
||||
:title="isPreviewMode ? '预览合同' : '编辑合同'"
|
||||
v-model="generateContractDialog.visible"
|
||||
width="1200px"
|
||||
align-center
|
||||
append-to-body
|
||||
class="dialog-form-layout"
|
||||
class="dialog-form-layout generate-print-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p v-if="!isPreviewMode" class="generate-contract-edit-hint">
|
||||
编辑模式:鼠标移入表格后,列/行边界会出现绿色调整条,按住拖动即可调列宽或行高;工具栏可合并拆分、插删行列、对齐与换行。调整后请点「保存模板」。
|
||||
</p>
|
||||
<template #header>
|
||||
<div class="generate-print-dialog-header">
|
||||
<span class="generate-print-dialog-title">{{ isPreviewMode ? '预览合同' : '编辑合同' }}</span>
|
||||
<p v-if="!isPreviewMode" class="generate-print-dialog-edit-hint">
|
||||
编辑模式:鼠标移入表格后,列/行边界会出现绿色调整条,按住拖动即可调列宽或行高;工具栏可合并拆分、插删行列、对齐与换行。调整后请点「保存模板」。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<TinyMceEditor
|
||||
v-model="contractContent"
|
||||
:height="600"
|
||||
@@ -803,6 +807,12 @@
|
||||
<el-button class="btn-ghost" @click="handleSaveTemplate">
|
||||
<el-icon><DocumentChecked /></el-icon>保存模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="toggleRedSeal">
|
||||
<el-icon>
|
||||
<component :is="showRedSeal ? Hide : Stamp" />
|
||||
</el-icon>
|
||||
{{ showRedSeal ? '隐藏红章' : '显示红章' }}
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handlePrintContract">
|
||||
<el-icon><Printer /></el-icon>打印
|
||||
</el-button>
|
||||
@@ -842,10 +852,12 @@ import {
|
||||
DocumentChecked,
|
||||
Download,
|
||||
Edit,
|
||||
Hide,
|
||||
InfoFilled,
|
||||
Plus,
|
||||
Printer,
|
||||
Promotion,
|
||||
Stamp,
|
||||
Upload,
|
||||
VideoPlay,
|
||||
View,
|
||||
@@ -939,6 +951,8 @@ const generateContractDialog = reactive({
|
||||
});
|
||||
const contractContent = ref('');
|
||||
const isPreviewMode = ref(false);
|
||||
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
|
||||
const showRedSeal = ref(true);
|
||||
const pdfGenerating = ref(false);
|
||||
const currentContractId = ref<string | number>();
|
||||
|
||||
@@ -1309,8 +1323,9 @@ const handleDetailPrint = async () => {
|
||||
console.error('替换占位符失败,使用原始内容:', replaceError);
|
||||
}
|
||||
|
||||
// 先设置内容,再打开对话框
|
||||
// 先设置内容,再打开对话框(每次打开默认显示红章)
|
||||
contractContent.value = htmlContent;
|
||||
showRedSeal.value = true;
|
||||
|
||||
// 关闭加载提示
|
||||
$modal.closeLoading();
|
||||
@@ -2551,6 +2566,80 @@ const replaceDynamicFields = (template: string, contractData: ContractVO): strin
|
||||
return result;
|
||||
};
|
||||
|
||||
const SEAL_IMG_RE = /<img[^>]*alt=["']印章["'][^>]*>/gi;
|
||||
|
||||
const getSupplierCodeFromContractNo = (contractNo?: string): string => {
|
||||
if (!contractNo) return 'SX';
|
||||
const match = contractNo.match(/^([A-Z]+)\/X-JT/);
|
||||
return match?.[1] || 'SX';
|
||||
};
|
||||
|
||||
const resolveSealImageByCompanyName = (companyName: string | undefined, supplierCode: string): string => {
|
||||
if (!companyName) return '';
|
||||
if (companyName.includes('云栋') || companyName.includes('安徽云栋化工有限公司')) {
|
||||
return '/src/assets/images/yd.png';
|
||||
}
|
||||
if (companyName.includes('山峡') || companyName.includes('安庆市山峡化工有限公司')) {
|
||||
return supplierCode === 'SX' ? '/src/assets/images/yz.png' : '/src/assets/images/yd.png';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const buildSealHtml = (companyName: string | undefined, supplierCode: string): string => {
|
||||
const sealSrc = resolveSealImageByCompanyName(companyName, supplierCode);
|
||||
if (!sealSrc) return '';
|
||||
return `<img src="${sealSrc}" alt="印章" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 140px; height: 140px; opacity: 0.85; z-index: 10;" />`;
|
||||
};
|
||||
|
||||
const stripSealImages = (html: string): string => html.replace(SEAL_IMG_RE, '');
|
||||
|
||||
const injectSealImages = (html: string, contractData: ContractVO): string => {
|
||||
let result = stripSealImages(html);
|
||||
const supplierCode = getSupplierCodeFromContractNo(contractData.contractNo);
|
||||
const partyASeal = buildSealHtml(contractData.partyA, supplierCode);
|
||||
const partyBSeal = buildSealHtml(contractData.partyB, supplierCode);
|
||||
|
||||
if (/<!--\s*印章图片[^>]*-->/.test(result)) {
|
||||
let sealCommentIndex = 0;
|
||||
return result.replace(/<!--\s*印章图片[^>]*-->/g, (match) => {
|
||||
sealCommentIndex += 1;
|
||||
if (sealCommentIndex === 1) return `${match}\n ${partyASeal}`;
|
||||
if (sealCommentIndex === 2) return `${match}\n ${partyBSeal}`;
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
if (partyASeal && contractData.partyA) {
|
||||
const escaped = escapeRegExp(contractData.partyA);
|
||||
result = result.replace(
|
||||
new RegExp(`(<div[^>]*>单位名称(章):\\s*${escaped}</div>)`),
|
||||
`${partyASeal}\n $1`
|
||||
);
|
||||
}
|
||||
if (partyBSeal && contractData.partyB) {
|
||||
const escaped = escapeRegExp(contractData.partyB);
|
||||
result = result.replace(
|
||||
new RegExp(`(<div[^>]*>单位名称(章):\\s*${escaped}</div>)`),
|
||||
`${partyBSeal}\n $1`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
|
||||
const toggleRedSeal = () => {
|
||||
if (showRedSeal.value) {
|
||||
contractContent.value = stripSealImages(contractContent.value);
|
||||
showRedSeal.value = false;
|
||||
return;
|
||||
}
|
||||
if (detailContract.value) {
|
||||
contractContent.value = injectSealImages(contractContent.value, detailContract.value);
|
||||
}
|
||||
showRedSeal.value = true;
|
||||
};
|
||||
|
||||
/** 切换预览/编辑模式 */
|
||||
const togglePreviewMode = () => {
|
||||
isPreviewMode.value = !isPreviewMode.value;
|
||||
@@ -2970,15 +3059,33 @@ onMounted(async () => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.generate-contract-edit-hint {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 12px;
|
||||
.generate-print-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
min-height: 24px;
|
||||
padding-right: 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generate-print-dialog-title {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.generate-print-dialog-edit-hint {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
border-radius: 8px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.parties-section {
|
||||
|
||||
@@ -470,7 +470,12 @@
|
||||
</template>
|
||||
|
||||
<!-- 产品选择器 -->
|
||||
<ProductSelector :visible="productSelectorVisible" @update:visible="productSelectorVisible = $event" @confirm="handleProductSelected" />
|
||||
<ProductSelector
|
||||
:visible="productSelectorVisible"
|
||||
:supplier-id="form.supplierId"
|
||||
@update:visible="productSelectorVisible = $event"
|
||||
@confirm="handleProductSelected"
|
||||
/>
|
||||
|
||||
<!-- 客户选择器 -->
|
||||
<CustomerSelector :visible="customerSelectorVisible" @update:visible="customerSelectorVisible = $event" @confirm="handleCustomerConfirm" />
|
||||
@@ -540,6 +545,7 @@ import ProductSelector from './ProductSelector.vue'
|
||||
import CustomerSelector from '@/components/CustomerSelector.vue'
|
||||
import ContactSelector from '@/components/ContactSelector.vue'
|
||||
import QuoteSelector from '@/components/QuoteSelector.vue'
|
||||
import { removeProductsNotAllowedByHazard } from '@/utils/crm-supplier-hazard'
|
||||
import ApprovalDialog from './ApprovalDialog.vue'
|
||||
import { getContactByCustomerId } from '@/api/crm/customer'
|
||||
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types'
|
||||
@@ -882,6 +888,8 @@ const loadContractDetail = async (id: number | string) => {
|
||||
form.value.products = contractData.products.map((product: any) => ({
|
||||
...product,
|
||||
productModel: product.model || '',
|
||||
isToxic: Number(product.isToxic || 0),
|
||||
isExplosive: Number(product.isExplosive || 0),
|
||||
priceTiers: (product.priceTiers || []).map((t: any) => {
|
||||
let ids: string[] = []
|
||||
if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
|
||||
@@ -1241,6 +1249,20 @@ const handleSupplierChange = async (supplierId: number | undefined) => {
|
||||
|
||||
updateContractNoBySupplier(supplier.supplierCode)
|
||||
rebuildContractName()
|
||||
|
||||
const allowToxic = Number((supplier as any).allowToxic || 0)
|
||||
const allowExplosive = Number((supplier as any).allowExplosive || 0)
|
||||
const { kept, removedNames } = removeProductsNotAllowedByHazard(
|
||||
form.value.products || [],
|
||||
allowToxic,
|
||||
allowExplosive
|
||||
)
|
||||
form.value.products = kept
|
||||
if (removedNames.length) {
|
||||
$modal.msgWarning(`当前主体不支持以下产品,已自动移除:${removedNames.join('、')}`)
|
||||
form.value.products.forEach((p) => calculateProductAmount(p))
|
||||
calculateTotalAmount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1457,6 +1479,10 @@ const handleSignDateChange = (val: string | null) => {
|
||||
|
||||
/** 添加产品 */
|
||||
const handleAddProduct = () => {
|
||||
if (!form.value.supplierId) {
|
||||
$modal.msgWarning('请先选择交易主体')
|
||||
return
|
||||
}
|
||||
productSelectorVisible.value = true
|
||||
}
|
||||
|
||||
@@ -1480,6 +1506,8 @@ const handleProductSelected = (products: any[]) => {
|
||||
deliveryMethod: '',
|
||||
description: product.description || '',
|
||||
remark: '',
|
||||
isToxic: Number(product.isToxic || 0),
|
||||
isExplosive: Number(product.isExplosive || 0),
|
||||
priceTiers: isFw ? [] : undefined
|
||||
})
|
||||
})
|
||||
|
||||
@@ -155,7 +155,8 @@ interface ProductCategoryVO {
|
||||
|
||||
// 定义组件属性
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
supplierId?: string | number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -216,7 +217,8 @@ const getList = async () => {
|
||||
pageSize: queryParams.value.pageSize,
|
||||
keyword: queryParams.value.productName,
|
||||
categoryId: queryParams.value.categoryId,
|
||||
status: queryParams.value.status
|
||||
status: queryParams.value.status,
|
||||
supplierId: props.supplierId != null && props.supplierId !== '' ? String(props.supplierId) : undefined
|
||||
});
|
||||
const data = (res as any)?.data || res || {};
|
||||
productList.value = data.list || data.rows || [];
|
||||
|
||||
@@ -978,21 +978,32 @@
|
||||
|
||||
<!-- 生成合同对话框 -->
|
||||
<el-dialog
|
||||
:title="isPreviewMode ? '预览合同' : '编辑合同'"
|
||||
v-model="generateContractDialog.visible"
|
||||
width="1200px"
|
||||
append-to-body
|
||||
class="generate-print-dialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p v-if="!isPreviewMode" class="generate-contract-edit-hint">
|
||||
编辑模式:鼠标移入表格后,列/行边界会出现绿色调整条,按住拖动即可调列宽或行高;工具栏可合并拆分、插删行列、对齐与换行。调整后请点「保存模板」。
|
||||
</p>
|
||||
<template #header>
|
||||
<div class="generate-print-dialog-header">
|
||||
<span class="generate-print-dialog-title">{{ isPreviewMode ? '预览合同' : '编辑合同' }}</span>
|
||||
<p v-if="!isPreviewMode" class="generate-print-dialog-edit-hint">
|
||||
编辑模式:鼠标移入表格后,列/行边界会出现绿色调整条,按住拖动即可调列宽或行高;工具栏可合并拆分、插删行列、对齐与换行。调整后请点「保存模板」。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<TinyMceEditor v-model="contractContent" :height="600" :disabled="isPreviewMode" :toolbar="!isPreviewMode" :contractNo="contract?.contractNo" />
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="generateContractDialog.visible = false">关 闭</el-button>
|
||||
<el-button v-if="hasContractPerm('edit')" type="info" icon="View" @click="togglePreviewMode">{{ isPreviewMode ? '编辑模式' : '预览模式' }}</el-button>
|
||||
<el-button v-if="hasContractPerm('edit')" type="warning" icon="DocumentChecked" @click="handleSaveTemplate">保存模板</el-button>
|
||||
<el-button v-if="hasContractPerm('print')" @click="toggleRedSeal">
|
||||
<el-icon>
|
||||
<component :is="showRedSeal ? Hide : Stamp" />
|
||||
</el-icon>
|
||||
{{ showRedSeal ? '隐藏红章' : '显示红章' }}
|
||||
</el-button>
|
||||
<el-button v-if="hasContractPerm('print')" type="success" icon="Printer" @click="handlePrintContract">打印</el-button>
|
||||
<el-button v-if="hasContractPerm('print')" type="primary" icon="Download" @click="handleGeneratePdfFile" :loading="pdfGenerating">生成PDF</el-button>
|
||||
</div>
|
||||
@@ -1012,7 +1023,7 @@ import { $modal } from '@/utils/modal';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import DictTag from '@/components/DictTag/index.vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { CircleCheck, CircleClose, View, ChatLineSquare, Clock, InfoFilled, VideoPlay, Document, Paperclip, Download, Back, Edit, Delete, Promotion, Printer } from '@element-plus/icons-vue';
|
||||
import { CircleCheck, CircleClose, View, ChatLineSquare, Clock, InfoFilled, VideoPlay, Document, Paperclip, Download, Back, Edit, Delete, Promotion, Printer, Hide, Stamp } from '@element-plus/icons-vue';
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue';
|
||||
import ExecuteDialog from './components/ExecuteDialog.vue';
|
||||
import InvalidDialog from './components/InvalidDialog.vue';
|
||||
@@ -1085,6 +1096,8 @@ const generateContractDialog = reactive({
|
||||
});
|
||||
const contractContent = ref('');
|
||||
const isPreviewMode = ref(false);
|
||||
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
|
||||
const showRedSeal = ref(true);
|
||||
const pdfGenerating = ref(false);
|
||||
|
||||
/** 加载合同详情 */
|
||||
@@ -2071,8 +2084,9 @@ const handlePrint = async () => {
|
||||
htmlContent = generateContractTemplate(contract.value);
|
||||
}
|
||||
|
||||
// 先设置内容,再打开对话框
|
||||
// 先设置内容,再打开对话框(每次打开默认显示红章)
|
||||
contractContent.value = htmlContent;
|
||||
showRedSeal.value = true;
|
||||
|
||||
// 关闭加载提示
|
||||
$modal.closeLoading();
|
||||
@@ -2529,6 +2543,80 @@ const handleDownloadHtml = () => {
|
||||
};
|
||||
|
||||
/** 切换预览/编辑模式 */
|
||||
const SEAL_IMG_RE = /<img[^>]*alt=["']印章["'][^>]*>/gi;
|
||||
|
||||
const getSupplierCodeFromContractNo = (contractNo?: string): string => {
|
||||
if (!contractNo) return 'SX';
|
||||
const match = contractNo.match(/^([A-Z]+)\/X-JT/);
|
||||
return match?.[1] || 'SX';
|
||||
};
|
||||
|
||||
const resolveSealImageByCompanyName = (companyName: string | undefined, supplierCode: string): string => {
|
||||
if (!companyName) return '';
|
||||
if (companyName.includes('云栋') || companyName.includes('安徽云栋化工有限公司')) {
|
||||
return '/src/assets/images/yd.png';
|
||||
}
|
||||
if (companyName.includes('山峡') || companyName.includes('安庆市山峡化工有限公司')) {
|
||||
return supplierCode === 'SX' ? '/src/assets/images/yz.png' : '/src/assets/images/yd.png';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const buildSealHtml = (companyName: string | undefined, supplierCode: string): string => {
|
||||
const sealSrc = resolveSealImageByCompanyName(companyName, supplierCode);
|
||||
if (!sealSrc) return '';
|
||||
return `<img src="${sealSrc}" alt="印章" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 140px; height: 140px; opacity: 0.85; z-index: 10;" />`;
|
||||
};
|
||||
|
||||
const stripSealImages = (html: string): string => html.replace(SEAL_IMG_RE, '');
|
||||
|
||||
const injectSealImages = (html: string, contractData: ContractVO): string => {
|
||||
let result = stripSealImages(html);
|
||||
const supplierCode = getSupplierCodeFromContractNo(contractData.contractNo);
|
||||
const partyASeal = buildSealHtml(contractData.partyA, supplierCode);
|
||||
const partyBSeal = buildSealHtml(contractData.partyB, supplierCode);
|
||||
|
||||
if (/<!--\s*印章图片[^>]*-->/.test(result)) {
|
||||
let sealCommentIndex = 0;
|
||||
return result.replace(/<!--\s*印章图片[^>]*-->/g, (match) => {
|
||||
sealCommentIndex += 1;
|
||||
if (sealCommentIndex === 1) return `${match}\n ${partyASeal}`;
|
||||
if (sealCommentIndex === 2) return `${match}\n ${partyBSeal}`;
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
if (partyASeal && contractData.partyA) {
|
||||
const escaped = escapeRegExp(contractData.partyA);
|
||||
result = result.replace(
|
||||
new RegExp(`(<div[^>]*>单位名称(章):\\s*${escaped}</div>)`),
|
||||
`${partyASeal}\n $1`
|
||||
);
|
||||
}
|
||||
if (partyBSeal && contractData.partyB) {
|
||||
const escaped = escapeRegExp(contractData.partyB);
|
||||
result = result.replace(
|
||||
new RegExp(`(<div[^>]*>单位名称(章):\\s*${escaped}</div>)`),
|
||||
`${partyBSeal}\n $1`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
|
||||
const toggleRedSeal = () => {
|
||||
if (showRedSeal.value) {
|
||||
contractContent.value = stripSealImages(contractContent.value);
|
||||
showRedSeal.value = false;
|
||||
return;
|
||||
}
|
||||
if (contract.value) {
|
||||
contractContent.value = injectSealImages(contractContent.value, contract.value);
|
||||
}
|
||||
showRedSeal.value = true;
|
||||
};
|
||||
|
||||
const togglePreviewMode = () => {
|
||||
isPreviewMode.value = !isPreviewMode.value;
|
||||
};
|
||||
@@ -3035,14 +3123,32 @@ const handleGeneratePdfFile = async () => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.generate-contract-edit-hint {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 12px;
|
||||
.generate-print-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
min-height: 24px;
|
||||
padding-right: 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generate-print-dialog-title {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.generate-print-dialog-edit-hint {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
border-radius: 8px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
@@ -536,7 +536,12 @@
|
||||
</section>
|
||||
|
||||
<!-- 产品选择器 -->
|
||||
<ProductSelector :visible="productSelectorVisible" @update:visible="productSelectorVisible = $event" @confirm="handleProductSelected" />
|
||||
<ProductSelector
|
||||
:visible="productSelectorVisible"
|
||||
:supplier-id="form.supplierId"
|
||||
@update:visible="productSelectorVisible = $event"
|
||||
@confirm="handleProductSelected"
|
||||
/>
|
||||
|
||||
<!-- 客户选择器 -->
|
||||
<CustomerSelector :visible="customerSelectorVisible" @update:visible="customerSelectorVisible = $event" @confirm="handleCustomerConfirm" />
|
||||
@@ -601,6 +606,7 @@ import type { CustomerVO, ContactVO } from '@/api/crm/customer/types';
|
||||
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo';
|
||||
import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo';
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm';
|
||||
import { removeProductsNotAllowedByHazard } from '@/utils/crm-supplier-hazard';
|
||||
|
||||
interface UserVO {
|
||||
userId: string | number
|
||||
@@ -847,6 +853,8 @@ const loadContractDetail = async (id: number | string) => {
|
||||
form.value.products = contractData.products.map((product: any) => ({
|
||||
...product,
|
||||
productModel: product.model || '',
|
||||
isToxic: Number(product.isToxic || 0),
|
||||
isExplosive: Number(product.isExplosive || 0),
|
||||
priceTiers: (product.priceTiers || []).map((t: any) => {
|
||||
let ids: string[] = [];
|
||||
if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
|
||||
@@ -1270,6 +1278,20 @@ const handleSupplierChange = async (supplierId: number | undefined) => {
|
||||
|
||||
// 更新合同名称中的供方简称
|
||||
updateContractNameBySupplier(supplier.shortName);
|
||||
|
||||
const allowToxic = Number((supplier as any).allowToxic || 0);
|
||||
const allowExplosive = Number((supplier as any).allowExplosive || 0);
|
||||
const { kept, removedNames } = removeProductsNotAllowedByHazard(
|
||||
form.value.products || [],
|
||||
allowToxic,
|
||||
allowExplosive
|
||||
);
|
||||
form.value.products = kept;
|
||||
if (removedNames.length) {
|
||||
$modal.msgWarning(`当前主体不支持以下产品,已自动移除:${removedNames.join('、')}`);
|
||||
form.value.products.forEach((p) => calculateProductAmount(p));
|
||||
calculateTotalAmount();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1668,6 +1690,10 @@ const handleSignDateChange = (val: string | null) => {
|
||||
|
||||
/** 添加产品 */
|
||||
const handleAddProduct = () => {
|
||||
if (!form.value.supplierId) {
|
||||
$modal.msgWarning('请先选择交易主体');
|
||||
return;
|
||||
}
|
||||
productSelectorVisible.value = true;
|
||||
};
|
||||
|
||||
@@ -1692,6 +1718,8 @@ const handleProductSelected = (products: any[]) => {
|
||||
deliveryMethod: '',
|
||||
description: product.description || '',
|
||||
remark: '',
|
||||
isToxic: Number(product.isToxic || 0),
|
||||
isExplosive: Number(product.isExplosive || 0),
|
||||
priceTiers: isFw ? [] : undefined
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,7 +138,8 @@ import { fetchProductList, fetchCategoryTree } from '@/api/crm/product';
|
||||
|
||||
// 定义组件属性
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
supplierId?: string | number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -199,7 +200,8 @@ const getList = async () => {
|
||||
pageSize: queryParams.value.pageSize,
|
||||
keyword: queryParams.value.productName,
|
||||
categoryId: queryParams.value.categoryId,
|
||||
status: queryParams.value.status
|
||||
status: queryParams.value.status,
|
||||
supplierId: props.supplierId != null && props.supplierId !== '' ? String(props.supplierId) : undefined
|
||||
});
|
||||
const data = (res as any)?.data || res || {};
|
||||
productList.value = data.list || data.rows || [];
|
||||
|
||||
@@ -367,7 +367,12 @@
|
||||
</el-form>
|
||||
|
||||
<!-- 产品选择器对话框 -->
|
||||
<ProductSelector :visible="productSelectorVisible" @update:visible="productSelectorVisible = $event" @confirm="handleProductConfirm" />
|
||||
<ProductSelector
|
||||
:visible="productSelectorVisible"
|
||||
:supplier-id="form.supplierId"
|
||||
@update:visible="productSelectorVisible = $event"
|
||||
@confirm="handleProductConfirm"
|
||||
/>
|
||||
|
||||
<!-- 客户选择器 -->
|
||||
<CustomerSelector :visible="customerSelectorVisible" @update:visible="customerSelectorVisible = $event" @confirm="handleCustomerConfirm" />
|
||||
@@ -471,6 +476,7 @@ import OpportunitySelector from '@/components/OpportunitySelector.vue';
|
||||
import { fetchUserOptions } from '@/api/admin';
|
||||
import { fetchContactsByCustomerId } from '@/api/crm/contact';
|
||||
import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo';
|
||||
import { removeProductsNotAllowedByHazard } from '@/utils/crm-supplier-hazard'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -789,7 +795,9 @@ const getQuoteInfo = async (id: string | number) => {
|
||||
taxRate: parseFloat(item.taxRate || '13'), // 税率(%)
|
||||
packageSpec: item.packageSpec || '', // 包装规格
|
||||
deliveryMethod: item.deliveryMethod || '', // 送货方式
|
||||
description: item.description
|
||||
description: item.description,
|
||||
isToxic: Number(item.isToxic || 0),
|
||||
isExplosive: Number(item.isExplosive || 0)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -868,7 +876,9 @@ const getQuoteInfoForCopy = async (copyId: string | number) => {
|
||||
taxRate: parseFloat(item.taxRate || '13'), // 税率(%)
|
||||
packageSpec: item.packageSpec || '', // 包装规格
|
||||
deliveryMethod: item.deliveryMethod || '', // 送货方式
|
||||
description: item.description
|
||||
description: item.description,
|
||||
isToxic: Number(item.isToxic || 0),
|
||||
isExplosive: Number(item.isExplosive || 0)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1127,11 +1137,26 @@ const handleSupplierChange = (supplierId: number | undefined) => {
|
||||
form.value.supplierId = supplier.id;
|
||||
form.value.supplierName = supplier.supplierName || '';
|
||||
updateQuoteTitleAndNo();
|
||||
const allowToxic = Number((supplier as any).allowToxic || 0)
|
||||
const allowExplosive = Number((supplier as any).allowExplosive || 0)
|
||||
const { kept, removedNames } = removeProductsNotAllowedByHazard(
|
||||
form.value.products || [],
|
||||
allowToxic,
|
||||
allowExplosive
|
||||
)
|
||||
form.value.products = kept
|
||||
if (removedNames.length) {
|
||||
$modal.msgWarning(`当前主体不支持以下产品,已自动移除:${removedNames.join('、')}`)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 添加产品 */
|
||||
const handleAddProduct = () => {
|
||||
if (!form.value.supplierId) {
|
||||
$modal.msgWarning('请先选择报价主体')
|
||||
return
|
||||
}
|
||||
productSelectorVisible.value = true;
|
||||
};
|
||||
|
||||
@@ -1152,7 +1177,9 @@ const handleProductConfirm = (products: any[]) => {
|
||||
taxAmount: 0,
|
||||
subtotal: 0,
|
||||
packageSpec: '',
|
||||
deliveryMethod: ''
|
||||
deliveryMethod: '',
|
||||
isToxic: Number(product.isToxic || 0),
|
||||
isExplosive: Number(product.isExplosive || 0)
|
||||
};
|
||||
form.value.products?.push(productForm);
|
||||
});
|
||||
|
||||
@@ -535,14 +535,21 @@
|
||||
<!-- 生成报价单对话框 -->
|
||||
<el-dialog
|
||||
v-model="generateQuoteDialog.visible"
|
||||
:title="isPreviewMode ? '预览报价单' : '编辑报价单'"
|
||||
width="1200px"
|
||||
align-center
|
||||
append-to-body
|
||||
class="dialog-form-layout"
|
||||
class="dialog-form-layout generate-print-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="generate-print-dialog-header">
|
||||
<span class="generate-print-dialog-title">{{ isPreviewMode ? '预览报价单' : '编辑报价单' }}</span>
|
||||
<p v-if="!isPreviewMode" class="generate-print-dialog-edit-hint">
|
||||
编辑模式:鼠标移入表格后,列/行边界会出现绿色调整条,按住拖动即可调列宽或行高;工具栏可合并拆分、插删行列、对齐与换行。调整后请点「保存模板」。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<TinyMceEditor v-model="quoteContent" :height="600" :disabled="isPreviewMode" :toolbar="!isPreviewMode" :supplier-code="currentSupplierCode" />
|
||||
<template #footer>
|
||||
<div class="generate-quote-dialog-actions">
|
||||
@@ -552,6 +559,12 @@
|
||||
<el-button class="btn-ghost" @click="handleSaveTemplate">
|
||||
<el-icon><DocumentChecked /></el-icon>保存模板
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="toggleRedSeal">
|
||||
<el-icon>
|
||||
<component :is="showRedSeal ? Hide : Stamp" />
|
||||
</el-icon>
|
||||
{{ showRedSeal ? '隐藏红章' : '显示红章' }}
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handlePrint">
|
||||
<el-icon><Printer /></el-icon>打印
|
||||
</el-button>
|
||||
@@ -723,11 +736,13 @@ import {
|
||||
DocumentCopy,
|
||||
Download,
|
||||
Edit,
|
||||
Hide,
|
||||
Plus,
|
||||
Printer,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
Select
|
||||
Select,
|
||||
Stamp
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
const dictStore = useDictStore()
|
||||
@@ -1084,6 +1099,8 @@ const generateQuoteDialog = reactive({
|
||||
});
|
||||
const quoteContent = ref('');
|
||||
const isPreviewMode = ref(false);
|
||||
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
|
||||
const showRedSeal = ref(true);
|
||||
const pdfGenerating = ref(false);
|
||||
const currentQuoteId = ref<string | number>(); // 当前报价单ID,用于保存模板
|
||||
const currentSupplierCode = ref<string>('SX'); // 当前供应商编码,用于控制背景图片和Logo显示
|
||||
@@ -2085,8 +2102,9 @@ const handleGenerateQuote = async (row?: QuoteVO) => {
|
||||
|
||||
console.log('HTML内容预览:', htmlContent.substring(0, 200));
|
||||
|
||||
// 先设置内容,再打开对话框
|
||||
// 先设置内容,再打开对话框(每次打开默认显示红章)
|
||||
quoteContent.value = htmlContent;
|
||||
showRedSeal.value = true;
|
||||
console.log('quoteContent已设置');
|
||||
|
||||
// 关闭加载提示
|
||||
@@ -2102,6 +2120,41 @@ const handleGenerateQuote = async (row?: QuoteVO) => {
|
||||
}
|
||||
};
|
||||
|
||||
const SEAL_IMG_RE = /<img[^>]*alt=["']印章["'][^>]*>/gi;
|
||||
|
||||
const QUOTE_SEAL_IMG_STYLE =
|
||||
'position: absolute; left: 50%; top: calc(50% - 50px); transform: translate(-50%, -50%); width: 120px; max-width: 120px; max-height: 120px; height: auto; object-fit: contain; z-index: 10; pointer-events: none; opacity: 0.9;';
|
||||
|
||||
const buildQuoteSealHtml = (supplierCode: string): string => {
|
||||
const sealImg = supplierCode === 'SX' ? yzImg : supplierCode === 'YD' ? ydImg : null;
|
||||
if (!sealImg) return '';
|
||||
return `<img src="${sealImg}" alt="印章" style="${QUOTE_SEAL_IMG_STYLE}" />`;
|
||||
};
|
||||
|
||||
const stripSealImages = (html: string): string => html.replace(SEAL_IMG_RE, '');
|
||||
|
||||
const injectQuoteSealImages = (html: string, supplierCode: string): string => {
|
||||
let result = stripSealImages(html);
|
||||
const sealHtml = buildQuoteSealHtml(supplierCode);
|
||||
if (!sealHtml) return result;
|
||||
// 印章落在「送货方式」表头内;兼容 TinyMCE 改写后的空白/换行
|
||||
if (/<th[^>]*>[\s\S]*?送货方式[\s\S]*?<\/th>/i.test(result)) {
|
||||
result = result.replace(/(<th[^>]*>[\s\S]*?送货方式)([\s\S]*?)(<\/th>)/i, `$1$2${sealHtml}$3`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
|
||||
const toggleRedSeal = () => {
|
||||
if (showRedSeal.value) {
|
||||
quoteContent.value = stripSealImages(quoteContent.value);
|
||||
showRedSeal.value = false;
|
||||
return;
|
||||
}
|
||||
quoteContent.value = injectQuoteSealImages(quoteContent.value, currentSupplierCode.value);
|
||||
showRedSeal.value = true;
|
||||
};
|
||||
|
||||
/** 打印报价单 */
|
||||
const handlePrint = () => {
|
||||
// 根据供应商编码决定是否显示背景图片
|
||||
@@ -2742,4 +2795,33 @@ onMounted(() => {
|
||||
background-color: var(--mint-table-head, #f8fafc);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.generate-print-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
min-height: 24px;
|
||||
padding-right: 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.generate-print-dialog-title {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.generate-print-dialog-edit-hint {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<!-- 主体危化品能力配置 P01 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">主体危化品能力</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="主体名称">
|
||||
<el-input
|
||||
v-model="searchForm.supplierName"
|
||||
placeholder="请输入主体名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">配置报价主体/交易主体是否允许易制毒、易制爆产品。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table v-loading="loading" :data="tableData" class="list-table" stripe>
|
||||
<el-table-column prop="supplierName" label="主体名称" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="supplierCode" label="编码" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="允许易制毒" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="Number(row.allowToxic || 0) === 1"
|
||||
:disabled="!canEdit || savingId === String(row.id)"
|
||||
:loading="savingId === String(row.id)"
|
||||
@change="(val: boolean) => handleToggle(row, 'allowToxic', val)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="允许易制爆" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="Number(row.allowExplosive || 0) === 1"
|
||||
:disabled="!canEdit || savingId === String(row.id)"
|
||||
:loading="savingId === String(row.id)"
|
||||
@change="(val: boolean) => handleToggle(row, 'allowExplosive', val)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@size-change="loadData"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
listSupplierInfo,
|
||||
updateSupplierInfo,
|
||||
type SupplierInfoVO
|
||||
} from '@/api/crm/supplierInfo'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'CrmSupplierHazard' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const canEdit = computed(() => {
|
||||
const info = userStore.getUserInfo as any
|
||||
const buttons: string[] = info?.buttons || info?.permissions || []
|
||||
if (!Array.isArray(buttons) || buttons.length === 0) return true
|
||||
return (
|
||||
buttons.includes('*:*:*') ||
|
||||
buttons.includes('crm:supplierHazard:edit') ||
|
||||
buttons.includes('crm:supplierHazard:list')
|
||||
)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const savingId = ref('')
|
||||
const tableData = ref<SupplierInfoVO[]>([])
|
||||
const searchForm = reactive({
|
||||
supplierName: ''
|
||||
})
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listSupplierInfo({
|
||||
pageNum: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
supplierName: searchForm.supplierName || undefined,
|
||||
status: '0'
|
||||
})
|
||||
tableData.value = res.rows || []
|
||||
pagination.total = res.total || 0
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '加载失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.supplierName = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
async function handleToggle(row: SupplierInfoVO, field: 'allowToxic' | 'allowExplosive', val: boolean) {
|
||||
const next = val ? 1 : 0
|
||||
const prevToxic = Number(row.allowToxic || 0)
|
||||
const prevExplosive = Number(row.allowExplosive || 0)
|
||||
const payload = {
|
||||
id: String(row.id),
|
||||
supplierName: row.supplierName,
|
||||
allowToxic: field === 'allowToxic' ? next : prevToxic,
|
||||
allowExplosive: field === 'allowExplosive' ? next : prevExplosive
|
||||
}
|
||||
savingId.value = String(row.id)
|
||||
try {
|
||||
await updateSupplierInfo(payload)
|
||||
row[field] = next
|
||||
ElMessage.success('已保存')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
savingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
@@ -124,17 +124,24 @@
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedRows.length === 1 && selectedRows[0]?.purchasePlanId"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows[0]?.hasWeighingSlip"
|
||||
:disabled="
|
||||
selectedRows.length !== 1 ||
|
||||
!selectedRows[0]?.purchasePlanId ||
|
||||
!!selectedRows[0]?.hasWeighingSlip
|
||||
"
|
||||
@click="handleGenerateWeighing(selectedRows[0])"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
{{ selectedRows[0]?.hasWeighingSlip ? '已生成地磅单' : '生成地磅单' }}
|
||||
{{
|
||||
selectedRows.length === 1 && selectedRows[0]?.hasWeighingSlip
|
||||
? '已生成地磅单'
|
||||
: '生成地磅单'
|
||||
}}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedRows.length === 1 && selectedRows[0]?.purchasePlanId"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || !selectedRows[0]?.purchasePlanId"
|
||||
@click="handleCopyFillLink(selectedRows[0])"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
|
||||
@@ -97,13 +97,13 @@
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
<el-button
|
||||
v-if="selectedPlan && selectedPlan.followStatus !== 'cancelled'"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
:disabled="!selectedPlan || selectedPlan.followStatus === 'cancelled'"
|
||||
@click="handleToolbarArchive"
|
||||
>
|
||||
<el-icon><FolderChecked /></el-icon>{{ archiveButtonLabel(selectedPlan) }}
|
||||
<el-icon><FolderChecked /></el-icon>
|
||||
{{ selectedPlan ? archiveButtonLabel(selectedPlan) : '归档' }}
|
||||
</el-button>
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
|
||||
@@ -84,28 +84,57 @@
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
<el-button
|
||||
v-if="selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
:class="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? 'btn-main'
|
||||
: 'btn-ghost'
|
||||
"
|
||||
:type="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? 'primary'
|
||||
: undefined
|
||||
"
|
||||
:disabled="
|
||||
!selectedPlan ||
|
||||
!canManageCertReady(selectedPlan) ||
|
||||
isHazardCertReady(selectedPlan)
|
||||
"
|
||||
:loading="certReadyLoading"
|
||||
@click="handleToolbarMarkCertReady"
|
||||
>
|
||||
<el-icon><CircleCheck /></el-icon>证书齐备
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedPlan && canUploadCert(selectedPlan)"
|
||||
:class="selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan) ? 'btn-ghost' : 'btn-main'"
|
||||
:type="selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan) ? undefined : 'primary'"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
:class="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? 'btn-ghost'
|
||||
: 'btn-main'
|
||||
"
|
||||
:type="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? undefined
|
||||
: 'primary'
|
||||
"
|
||||
:disabled="!selectedPlan || !canUploadCert(selectedPlan)"
|
||||
@click="handleToolbarUploadCert"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>上传证书
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedPlan && canManageCertReady(selectedPlan) && isHazardCertReady(selectedPlan)"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
:disabled="
|
||||
!selectedPlan ||
|
||||
!canManageCertReady(selectedPlan) ||
|
||||
!isHazardCertReady(selectedPlan)
|
||||
"
|
||||
:loading="certReadyLoading"
|
||||
@click="handleToolbarUnmarkCertReady"
|
||||
>
|
||||
|
||||
@@ -242,7 +242,6 @@ const formData = reactive({
|
||||
|
||||
const formRules = {
|
||||
belongsCompany: [{ required: true, message: '请选择归属公司', trigger: 'change' }],
|
||||
planType: [{ required: true, message: '请选择单据类型', trigger: 'change' }],
|
||||
planDate: [{ required: true, message: '请选择计划日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user