提交
This commit is contained in:
1 parent
dd1acdc787
commit
4222eaa593
47 files changed
+1054350
-9663
No files matched your search
@@ -125,6 +125,7 @@
|
||||
hide-nav
|
||||
:tab="viewTab"
|
||||
show-archive-io-fields
|
||||
show-actual-qty-disclaimer
|
||||
feedback-editable
|
||||
:arrange-driver-url="driverUrl"
|
||||
:arrange-site-records="siteRecords"
|
||||
@@ -211,7 +212,7 @@
|
||||
const productRows = ref<PlanFormProductRow[]>([])
|
||||
|
||||
const formData = reactive({
|
||||
hasFreight: 0,
|
||||
hasFreight: 1,
|
||||
distanceKm: null as number | null,
|
||||
freightType: 'by_weight'
|
||||
})
|
||||
@@ -413,7 +414,7 @@
|
||||
plan.value = {}
|
||||
productRows.value = []
|
||||
Object.assign(formData, {
|
||||
hasFreight: 0,
|
||||
hasFreight: 1,
|
||||
distanceKm: null,
|
||||
freightType: 'by_weight'
|
||||
})
|
||||
@@ -422,7 +423,7 @@
|
||||
}
|
||||
|
||||
async function applyFreightPrefill(data: Plan) {
|
||||
formData.hasFreight = data.hasFreight ?? 0
|
||||
formData.hasFreight = isAdjustMode.value ? (data.hasFreight ?? 0) : 1
|
||||
freightCalc.resetFreightCalc()
|
||||
|
||||
if (data.freightAmount != null && data.hasFreight === 1) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="!isEdit"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -589,7 +590,11 @@ function resolveCurrentUserName() {
|
||||
}
|
||||
|
||||
function todayYmd(): string {
|
||||
return new Date().toISOString().split('T')[0]
|
||||
const d = new Date()
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
const isEdit = computed(() => !!props.planId)
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
displayFeedbackTime || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<p v-if="!hasDriverFeedback" class="pl-plan-form-empty-hint pl-feedback-ticket-empty"
|
||||
<p
|
||||
v-if="!hasDriverFeedback && !driverImagesEditable"
|
||||
class="pl-plan-form-empty-hint pl-feedback-ticket-empty"
|
||||
>暂无司机回传</p
|
||||
>
|
||||
<template v-else>
|
||||
<template v-if="hasDriverFeedback">
|
||||
<div
|
||||
v-for="row in driverTicketQtyRows"
|
||||
:key="row.key"
|
||||
@@ -40,6 +42,9 @@
|
||||
<div>
|
||||
<div class="pl-feedback-ticket-lab">实际数量({{ feedbackQtyUnit }})</div>
|
||||
<div class="pl-feedback-ticket-val">{{ row.actualQty }}</div>
|
||||
<div v-if="showActualQtyDisclaimer" class="pl-feedback-ticket-val-tip"
|
||||
>(仅供参考,实际以附件中数量为准)</div
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<div class="pl-feedback-ticket-lab">品牌数量</div>
|
||||
@@ -55,18 +60,23 @@
|
||||
<span class="pl-feedback-ticket-text">{{ feedbackRemark || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-feedback-ticket-foot">
|
||||
<span class="pl-feedback-ticket-lab">附件</span>
|
||||
<AttachmentUploadField
|
||||
v-if="feedbackImageCsv"
|
||||
class="pl-feedback-ticket-attach"
|
||||
:model-value="feedbackImageCsv"
|
||||
compact
|
||||
disabled
|
||||
/>
|
||||
<span v-else class="pl-plan-form-empty-hint">—</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="pl-feedback-ticket-foot">
|
||||
<span class="pl-feedback-ticket-lab">附件</span>
|
||||
<AttachmentUploadField
|
||||
v-if="driverImagesEditable || feedbackImageCsv"
|
||||
class="pl-feedback-ticket-attach"
|
||||
:class="{ 'pl-feedback-ticket-attach--editable': driverImagesEditable }"
|
||||
:model-value="feedbackImageCsv"
|
||||
:disabled="!driverImagesEditable"
|
||||
compact
|
||||
folder="pl/transport/feedback"
|
||||
accept="image/*,video/*,.pdf"
|
||||
:limit="15"
|
||||
@update:model-value="handleDriverImagesChange"
|
||||
/>
|
||||
<span v-else class="pl-plan-form-empty-hint">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="feedbackEditable" class="pl-plan-feedback-card-footer">
|
||||
@@ -106,6 +116,9 @@
|
||||
<div>
|
||||
<div class="pl-feedback-ticket-lab">实际数量({{ feedbackQtyUnit }})</div>
|
||||
<div class="pl-feedback-ticket-val">{{ row.actualQty }}</div>
|
||||
<div v-if="showActualQtyDisclaimer" class="pl-feedback-ticket-val-tip"
|
||||
>(仅供参考,实际以附件中数量为准)</div
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<div class="pl-feedback-ticket-lab">品牌数量</div>
|
||||
@@ -148,11 +161,16 @@
|
||||
<div class="pl-feedback-ticket-foot">
|
||||
<span class="pl-feedback-ticket-lab">附件</span>
|
||||
<AttachmentUploadField
|
||||
v-if="ioOrderImageCsv"
|
||||
v-if="ioOrderImagesEditable || ioOrderImageCsv"
|
||||
class="pl-feedback-ticket-attach"
|
||||
:class="{ 'pl-feedback-ticket-attach--editable': ioOrderImagesEditable }"
|
||||
:model-value="ioOrderImageCsv"
|
||||
:disabled="!ioOrderImagesEditable"
|
||||
compact
|
||||
disabled
|
||||
folder="pl/transport/site-feedback"
|
||||
accept="image/*,video/*,.pdf"
|
||||
:limit="15"
|
||||
@update:model-value="handleIoOrderImagesChange"
|
||||
/>
|
||||
<span v-else class="pl-plan-form-empty-hint">—</span>
|
||||
</div>
|
||||
@@ -386,8 +404,12 @@
|
||||
feedbackEditable?: boolean
|
||||
/** 仅运输计划归档:展示入库/出库单价与金额 */
|
||||
showArchiveIoFields?: boolean
|
||||
/** 司机/出库单回传「实际数量」后展示仅供参考说明 */
|
||||
showActualQtyDisclaimer?: boolean
|
||||
loading?: boolean
|
||||
canEditDriverFeedback?: boolean
|
||||
driverImagesEditable?: boolean
|
||||
ioOrderImagesEditable?: boolean
|
||||
hasDriverFeedback?: boolean
|
||||
arrangeDriverUrl?: string
|
||||
driverFeedbackTableRows?: DriverFeedbackTableRow[]
|
||||
@@ -407,8 +429,11 @@
|
||||
{
|
||||
feedbackEditable: false,
|
||||
showArchiveIoFields: false,
|
||||
showActualQtyDisclaimer: false,
|
||||
loading: false,
|
||||
canEditDriverFeedback: false,
|
||||
driverImagesEditable: false,
|
||||
ioOrderImagesEditable: false,
|
||||
hasDriverFeedback: false,
|
||||
arrangeDriverUrl: '',
|
||||
driverFeedbackTableRows: () => [],
|
||||
@@ -532,8 +557,20 @@
|
||||
'copy-site-url': [url?: string]
|
||||
'edit-site-feedback': [row: SiteFeedbackTableRow]
|
||||
'copy-site-summary': [row: SiteFeedbackTableRow]
|
||||
'save-driver-images': [csv: string]
|
||||
'save-io-order-images': [csv: string]
|
||||
}>()
|
||||
|
||||
function handleDriverImagesChange(csv: string) {
|
||||
if (!props.driverImagesEditable) return
|
||||
emit('save-driver-images', csv)
|
||||
}
|
||||
|
||||
function handleIoOrderImagesChange(csv: string) {
|
||||
if (!props.ioOrderImagesEditable) return
|
||||
emit('save-io-order-images', csv)
|
||||
}
|
||||
|
||||
const ioSectionTitle = computed(() => {
|
||||
if (!props.ioOrder) return '现场回传信息'
|
||||
return props.ioOrder.kind === 'inbound' ? '入库单回传' : '出库单回传'
|
||||
@@ -824,6 +861,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 可编辑:沿用紧凑上传卡片尺寸,与只读缩略图同排展示 */
|
||||
.pl-feedback-ticket-attach--editable {
|
||||
:deep(.attachment-item) {
|
||||
width: 88px;
|
||||
}
|
||||
|
||||
:deep(.attachment-preview) {
|
||||
height: 66px;
|
||||
}
|
||||
|
||||
:deep(.attachment-preview__icon) {
|
||||
width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-feedback-ticket-band {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -875,6 +928,14 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pl-feedback-ticket-val-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
}
|
||||
|
||||
.pl-feedback-ticket-val {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -392,6 +392,7 @@
|
||||
import {
|
||||
inboundOrderApi,
|
||||
outboundOrderApi,
|
||||
planApi,
|
||||
type PlIoOrder,
|
||||
type TransportSiteLink
|
||||
} from '@/api/pl'
|
||||
@@ -440,6 +441,8 @@
|
||||
tab?: string
|
||||
/** 仅运输计划归档:展示入库/出库单价与金额 */
|
||||
showArchiveIoFields?: boolean
|
||||
/** 司机/出库单回传「实际数量」后展示仅供参考说明 */
|
||||
showActualQtyDisclaimer?: boolean
|
||||
/** 计划概况产品表:展示入库/出库数量(不含单价与金额) */
|
||||
showInboundOutboundQty?: boolean
|
||||
}>(),
|
||||
@@ -453,6 +456,7 @@
|
||||
hideNav: false,
|
||||
tab: undefined,
|
||||
showArchiveIoFields: false,
|
||||
showActualQtyDisclaimer: false,
|
||||
showInboundOutboundQty: false
|
||||
}
|
||||
)
|
||||
@@ -580,6 +584,8 @@
|
||||
|
||||
const ioOrderEditVisible = ref(false)
|
||||
const ioOrderSaving = ref(false)
|
||||
const driverImagesSaving = ref(false)
|
||||
const ioOrderImagesSaving = ref(false)
|
||||
const ioOrderEditForm = ref<PlIoOrder | null>(null)
|
||||
const ioOrderEditImagesCsv = ref('')
|
||||
|
||||
@@ -1092,6 +1098,60 @@
|
||||
reload()
|
||||
}
|
||||
|
||||
async function saveDriverImagesInline(csv: string) {
|
||||
if (!props.planId || driverImagesSaving.value) return
|
||||
const nextList = csv
|
||||
? csv
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
if (nextList.join(',') === feedbackImageList.value.join(',')) return
|
||||
|
||||
driverImagesSaving.value = true
|
||||
try {
|
||||
await planApi.updateDriverFeedback(props.planId, {
|
||||
feedbackImages: nextList
|
||||
})
|
||||
ElMessage.success('司机回传附件已保存')
|
||||
reload()
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '保存失败')
|
||||
} finally {
|
||||
driverImagesSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveIoOrderImagesInline(csv: string) {
|
||||
const order = ioOrder.value
|
||||
if (!order || ioOrderImagesSaving.value || order.status === 'voided') return
|
||||
const nextList = csv
|
||||
? csv
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
const currentList = Array.isArray(order.siteFeedbackImages)
|
||||
? order.siteFeedbackImages.filter(Boolean)
|
||||
: []
|
||||
if (nextList.join(',') === currentList.join(',')) return
|
||||
|
||||
const api = order.kind === 'inbound' ? inboundOrderApi : outboundOrderApi
|
||||
const label = order.kind === 'inbound' ? '入库单' : '出库单'
|
||||
ioOrderImagesSaving.value = true
|
||||
try {
|
||||
await api.patch(order.id, {
|
||||
siteFeedbackImages: nextList
|
||||
})
|
||||
ElMessage.success(`${label}回传附件已保存`)
|
||||
reload()
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '保存失败')
|
||||
} finally {
|
||||
ioOrderImagesSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const hasArrangeSiteLinks = computed(
|
||||
() => props.showArrangeLinks && (props.arrangeSiteRecords?.length ?? 0) > 0
|
||||
)
|
||||
@@ -1149,8 +1209,13 @@
|
||||
const feedbackBlocksBind = computed(() => ({
|
||||
feedbackEditable: props.feedbackEditable,
|
||||
showArchiveIoFields: props.showArchiveIoFields,
|
||||
showActualQtyDisclaimer: props.showActualQtyDisclaimer,
|
||||
loading: loading.value,
|
||||
canEditDriverFeedback: canEditDriverFeedback.value,
|
||||
driverImagesEditable: props.feedbackEditable && canEditDriverFeedback.value,
|
||||
ioOrderImagesEditable: Boolean(
|
||||
props.feedbackEditable && ioOrder.value && ioOrder.value.status !== 'voided'
|
||||
),
|
||||
hasDriverFeedback: hasDriverFeedback.value,
|
||||
arrangeDriverUrl: props.arrangeDriverUrl,
|
||||
driverFeedbackTableRows: driverFeedbackTableRows.value,
|
||||
@@ -1172,6 +1237,12 @@
|
||||
'edit-driver': openDriverFeedbackDialog,
|
||||
'copy-driver-url': copyArrangeDriverUrl,
|
||||
'edit-io-order': openIoOrderEditDialog,
|
||||
'save-driver-images': (csv: string) => {
|
||||
void saveDriverImagesInline(csv)
|
||||
},
|
||||
'save-io-order-images': (csv: string) => {
|
||||
void saveIoOrderImagesInline(csv)
|
||||
},
|
||||
'copy-site-url': (url?: string) => {
|
||||
void copyArrangeSiteUrl(url)
|
||||
},
|
||||
|
||||
@@ -26,15 +26,13 @@ const LOGO_LEFT = 50
|
||||
const TITLE_STYLE = `${FONT_TITLE}font-weight:700 !important;letter-spacing:6px;line-height:1.35;`
|
||||
const TITLE_SUB_STYLE = `${TITLE_STYLE}margin-top:2px;letter-spacing:10px;`
|
||||
const TABLE_ROW_PADDING = '4px'
|
||||
/** 信息表列宽:收货单位(4) / 联系方式(1,略加宽) / 日期(2) */
|
||||
/** 信息表列宽:左栏 3/5(收货单位、收货联系方式)/ 右栏 2/5(单据编号、日期) */
|
||||
const INFO_COLGROUP = `<colgroup>
|
||||
<col style="width:12.75%;" />
|
||||
<col style="width:12.75%;" />
|
||||
<col style="width:12.75%;" />
|
||||
<col style="width:12.75%;" />
|
||||
<col style="width:22.5%;" />
|
||||
<col style="width:13.25%;" />
|
||||
<col style="width:13.25%;" />
|
||||
<col style="width:20%;" />
|
||||
<col style="width:20%;" />
|
||||
<col style="width:20%;" />
|
||||
<col style="width:20%;" />
|
||||
<col style="width:20%;" />
|
||||
</colgroup>`
|
||||
/**
|
||||
* 物料表列宽:单据编号 + 加宽编号/名称,收窄规格/单位;前 5 列合计供条款区使用。
|
||||
@@ -628,22 +626,30 @@ 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="4" style="${INFO_CELL_LEFT}">
|
||||
<td colspan="3" 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}} </span>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="1" style="${INFO_CELL_LEFT}">
|
||||
<td colspan="2" 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-label" style="${INFO_LABEL}">单据编号:</span>
|
||||
<span class="delivery-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planNo}} </span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" 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}} </span>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="2" 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-label" style="${INFO_LABEL}">日 期:</span>
|
||||
<span class="delivery-slip-info-value-center" style="${INFO_VALUE_INLINE_CENTER}">{{planDateChinese}} </span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -1302,7 +1308,7 @@ function normalizeDeliverySlipGapBeforeProduct(html: string): string {
|
||||
|
||||
return html.replace(
|
||||
new RegExp(
|
||||
`(<span class="delivery-slip-info-label"[^>]*>日(?: )?期:<\\/span>[\\s\\S]*?<\\/tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr[\\s\\S]*?<th[^>]*>${productHeader}|<tr[\\s\\S]*?<th[^>]*>${productHeader})`
|
||||
`(<span class="delivery-slip-info-label"[^>]*>日(?: )*期:<\\/span>[\\s\\S]*?<\\/tr>\\s*)(<!--\\s*中:产品明细表[\\s\\S]*?<tr[\\s\\S]*?<th[^>]*>${productHeader}|<tr[\\s\\S]*?<th[^>]*>${productHeader})`
|
||||
),
|
||||
`$1${gapRow}$2`
|
||||
)
|
||||
@@ -1419,34 +1425,39 @@ function normalizeDeliverySlipSignRow(html: string): string {
|
||||
return normalizeDeliverySlipSignLabelMarkup(result)
|
||||
}
|
||||
|
||||
/** 信息区:收货单位 / 联系方式 / 日期 同一行(兼容旧多行布局) */
|
||||
/** 信息区:收货单位+单据编号 / 收货联系方式+日期 两行 3/5+2/5(兼容旧单行布局) */
|
||||
function normalizeDeliverySlipInfoLayout(html: string): string {
|
||||
if (!html.includes('delivery-slip-info-table')) return html
|
||||
|
||||
let result = html.replace(/收货联系方式:/g, '联系方式:')
|
||||
let result = html.replace(/(收货)?联系方式:/g, '收货联系方式:')
|
||||
|
||||
const infoMatch = result.match(/<table class="delivery-slip-info-table"[^>]*>[\s\S]*?<\/table>/i)
|
||||
if (!infoMatch) return result
|
||||
|
||||
const infoHtml = infoMatch[0]
|
||||
const rowCount = (infoHtml.match(/<tr[\s>]/g) || []).length
|
||||
const hasPlanNoInInfo = infoHtml.includes('单据编号:')
|
||||
const layoutOk =
|
||||
rowCount === 1 &&
|
||||
rowCount === 2 &&
|
||||
infoHtml.includes('收货单位:') &&
|
||||
infoHtml.includes('联系方式:') &&
|
||||
/日(?: )?期:/.test(infoHtml) &&
|
||||
!hasPlanNoInInfo &&
|
||||
/<td[^>]*colspan="4"[^>]*>[\s\S]*?收货单位:/.test(infoHtml) &&
|
||||
/<td[^>]*colspan="1"[^>]*>[\s\S]*?联系方式:/.test(infoHtml) &&
|
||||
/<td[^>]*colspan="2"[^>]*>[\s\S]*?日(?: )?期:/.test(infoHtml) &&
|
||||
!/<td[^>]*colspan="[2-5]"[^>]*>[\s\S]*?联系方式:/.test(infoHtml)
|
||||
infoHtml.includes('单据编号:') &&
|
||||
infoHtml.includes('收货联系方式:') &&
|
||||
/日(?: )*期:/.test(infoHtml) &&
|
||||
/<td[^>]*colspan="3"[^>]*>[\s\S]*?收货单位:/.test(infoHtml) &&
|
||||
/<td[^>]*colspan="2"[^>]*>[\s\S]*?单据编号:/.test(infoHtml) &&
|
||||
/<td[^>]*colspan="3"[^>]*>[\s\S]*?收货联系方式:/.test(infoHtml) &&
|
||||
/<td[^>]*colspan="2"[^>]*>[\s\S]*?日(?: )*期:/.test(infoHtml)
|
||||
|
||||
if (layoutOk) {
|
||||
return result.replace(
|
||||
let next = result.replace(
|
||||
/(<table class="delivery-slip-info-table"[^>]*>\s*)(?:<colgroup>[\s\S]*?<\/colgroup>\s*)?/i,
|
||||
`$1${INFO_COLGROUP}`
|
||||
)
|
||||
// 单据编号值居中(兼容先前左对齐模板)
|
||||
next = next.replace(
|
||||
/(<span class="delivery-slip-info-label"[^>]*>单据编号:<\/span>\s*<span class=")delivery-slip-info-value-(?:left|center)(" style=")[^"]*("[^>]*>)/,
|
||||
`$1delivery-slip-info-value-center$2${INFO_VALUE_INLINE_CENTER}$3`
|
||||
)
|
||||
return next
|
||||
}
|
||||
|
||||
return result.replace(infoMatch[0], buildDeliverySlipInfoTableHtml())
|
||||
@@ -1621,7 +1632,7 @@ function normalizeDeliverySlipDocLayout(html: string): string {
|
||||
.replace(/top:4px;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px;`)
|
||||
.replace(/top:-96px !important;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px !important;`)
|
||||
.replace(/top:-96px;/g, `top:${DELIVERY_SLIP_SEAL_TOP}px;`)
|
||||
.replace(/>日期:</g, '>日 期:<')
|
||||
.replace(/>日(?: )*期:</g, '>日 期:<')
|
||||
.replace(/border-bottom:2px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
|
||||
.replace(/border-bottom:1px solid #000 !important;/g, `border-bottom:${INFO_UNDERLINE} !important;`)
|
||||
.replace(/border:2px solid #000 !important;/g, `border:${PRODUCT_GRID_BORDER} !important;`)
|
||||
@@ -1708,38 +1719,38 @@ function normalizeDeliverySlipTypeCheckRow(html: string): string {
|
||||
if (!result.includes('delivery-slip-type-check-row')) {
|
||||
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"[^>]*>日(?: )?期:)/,
|
||||
// 优先插在单据编号上方(当前布局:右栏 2/5)
|
||||
const withPlanNo = result.replace(
|
||||
/(<td colspan="2"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>单据编号:)/,
|
||||
`$1${checkRowHtml}\n $2`
|
||||
)
|
||||
if (withClass !== result) {
|
||||
result = withClass
|
||||
if (withPlanNo !== result) {
|
||||
result = withPlanNo
|
||||
} else {
|
||||
result = result.replace(
|
||||
/(<td colspan="3"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?: )?期:)/,
|
||||
`$1${checkRowHtml}\n $2`
|
||||
)
|
||||
}
|
||||
// 兼容旧模板:日期在 colspan=2 / colspan=1 单元格时仍插多选
|
||||
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"[^>]*>日(?: )?期:)/,
|
||||
`$1${checkRowHtml}\n $2`
|
||||
)
|
||||
}
|
||||
if (!result.includes('delivery-slip-type-check-row')) {
|
||||
result = result.replace(
|
||||
/(<td colspan="1"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?: )?期:)/,
|
||||
`$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`
|
||||
)
|
||||
}
|
||||
// 兼容旧模板:日期单元格仍插多选,随后由 infoLayout 迁移
|
||||
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"[^>]*>日(?: )*期:)/,
|
||||
`$1${checkRowHtml}\n $2`
|
||||
)
|
||||
}
|
||||
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`
|
||||
)
|
||||
}
|
||||
if (!result.includes('delivery-slip-type-check-row')) {
|
||||
result = result.replace(
|
||||
/(<td colspan="1"[^>]*>\s*)(<div class="delivery-slip-info-row"[^>]*>\s*<span class="delivery-slip-info-label"[^>]*>日(?: )*期:)/,
|
||||
`$1${checkRowHtml}\n $2`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return refreshDeliverySlipTypeCheckRowStyle(result)
|
||||
|
||||
@@ -103,6 +103,9 @@
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="single" @click="handleCopy()">
|
||||
<el-icon><DocumentCopy /></el-icon>合同复制
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!canSubmit" @click="handleSubmitBatch">
|
||||
<el-icon><Document /></el-icon>提交审批
|
||||
</el-button>
|
||||
@@ -516,13 +519,14 @@
|
||||
v-if="isManageMode"
|
||||
v-model:visible="formDialogVisible"
|
||||
:edit-id="formDialogEditId"
|
||||
:copy-source="formDialogCopySource"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ContractListPage">
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||
import {
|
||||
listContract,
|
||||
getContract as getContractDetail,
|
||||
@@ -536,6 +540,7 @@ import {
|
||||
Clock,
|
||||
Close,
|
||||
Document,
|
||||
DocumentCopy,
|
||||
Download,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
@@ -606,6 +611,7 @@ const resolveCurrentUserId = (): string => {
|
||||
// 合同表单对话框状态
|
||||
const formDialogVisible = ref(false)
|
||||
const formDialogEditId = ref<string | null>(null)
|
||||
const formDialogCopySource = ref<ContractVO | null>(null)
|
||||
|
||||
// 列配置
|
||||
const listColumnDefs = [
|
||||
@@ -1087,15 +1093,50 @@ const handleExpandChange = async (row: ContractVO, expandedRows: ContractVO[]) =
|
||||
}
|
||||
};
|
||||
|
||||
const resetFormDialogState = () => {
|
||||
formDialogEditId.value = null
|
||||
formDialogCopySource.value = null
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
formDialogEditId.value = null
|
||||
resetFormDialogState()
|
||||
formDialogVisible.value = true
|
||||
};
|
||||
}
|
||||
|
||||
/** 解析当前唯一选中的合同行(优先列表数据,保证含 products) */
|
||||
const resolveSingleSelectedContract = (row?: ContractVO): ContractVO | undefined => {
|
||||
if (row?.id != null && String(row.id) !== '') {
|
||||
return contractList.value.find((item) => String(item.id) === String(row.id)) || row
|
||||
}
|
||||
if (ids.value.length !== 1) return undefined
|
||||
const selectedId = ids.value[0]
|
||||
return (
|
||||
contractList.value.find((item) => String(item.id) === String(selectedId))
|
||||
|| selectedRows.value.find((item) => String(item.id) === String(selectedId))
|
||||
)
|
||||
}
|
||||
|
||||
/** 合同复制:选中一条后打开表单,仅带入产品明细 */
|
||||
const handleCopy = (row?: ContractVO) => {
|
||||
if (!row && ids.value.length !== 1) {
|
||||
$modal.msgWarning('请选择一条数据进行合同复制')
|
||||
return
|
||||
}
|
||||
const targetRow = resolveSingleSelectedContract(row)
|
||||
if (!targetRow?.id) {
|
||||
$modal.msgWarning('未找到选中的数据')
|
||||
return
|
||||
}
|
||||
formDialogEditId.value = null
|
||||
formDialogCopySource.value = targetRow
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 表单对话框保存/提交成功回调 */
|
||||
const handleFormSuccess = () => {
|
||||
formDialogVisible.value = false
|
||||
resetFormDialogState()
|
||||
getList()
|
||||
}
|
||||
|
||||
@@ -1300,7 +1341,13 @@ onMounted(() => {
|
||||
} else {
|
||||
handleViewChange()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
watch(formDialogVisible, (visible) => {
|
||||
if (!visible) {
|
||||
resetFormDialogState()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
>{{ dictStore.getLabel('crm_contract_status', detailContract?.status) }}</el-tag>
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<el-button
|
||||
v-if="manageActions"
|
||||
class="btn-ghost"
|
||||
@click="handleDetailCopy()"
|
||||
>
|
||||
<el-icon><DocumentCopy /></el-icon>合同复制
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="manageActions && isContractEditableStatus(detailContract?.status)"
|
||||
class="btn-ghost"
|
||||
@@ -844,6 +851,7 @@
|
||||
<ContractFormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:edit-id="formDialogEditId"
|
||||
:copy-source="formDialogCopySource"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
<CustomerDetailDialog v-if="customerDialogActive" ref="customerDetailRef" />
|
||||
@@ -853,10 +861,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { ref, reactive, computed, onMounted, defineAsyncComponent, nextTick, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { isFrameworkContractType, isContractEditableStatus, getContractReeditConfirmMessage } from '../contractType'
|
||||
import { displayFrameworkMoney, displayFrameworkQuantity } from '../frameworkProductPricing'
|
||||
import {
|
||||
displayFrameworkMoney,
|
||||
displayFrameworkQuantity,
|
||||
formatFrameworkQuantityForPrint
|
||||
} from '../frameworkProductPricing'
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
@@ -865,6 +877,7 @@ import {
|
||||
Close,
|
||||
Delete,
|
||||
Document,
|
||||
DocumentCopy,
|
||||
DocumentChecked,
|
||||
Download,
|
||||
Edit,
|
||||
@@ -984,6 +997,18 @@ const currentContractId = ref<string | number>();
|
||||
|
||||
const formDialogVisible = ref(false);
|
||||
const formDialogEditId = ref<string | null>(null);
|
||||
const formDialogCopySource = ref<ContractVO | null>(null);
|
||||
|
||||
const resetFormDialogState = () => {
|
||||
formDialogEditId.value = null
|
||||
formDialogCopySource.value = null
|
||||
}
|
||||
|
||||
watch(formDialogVisible, (open) => {
|
||||
if (!open) {
|
||||
resetFormDialogState()
|
||||
}
|
||||
})
|
||||
|
||||
const approvalDialogRef = ref();
|
||||
const executeDialogRef = ref();
|
||||
@@ -1050,6 +1075,7 @@ function handleActionSuccess() {
|
||||
|
||||
function handleFormSuccess() {
|
||||
formDialogVisible.value = false;
|
||||
resetFormDialogState();
|
||||
loadDetailData();
|
||||
emit('success');
|
||||
}
|
||||
@@ -1258,6 +1284,18 @@ const handleDetailDrawerClose = () => {
|
||||
detailAttachmentFileList.value = [];
|
||||
};
|
||||
|
||||
/** 详情抽屉 - 合同复制 */
|
||||
const handleDetailCopy = () => {
|
||||
if (!detailContract.value?.id) {
|
||||
$modal.msgWarning('未找到合同数据')
|
||||
return
|
||||
}
|
||||
visible.value = false
|
||||
formDialogEditId.value = null
|
||||
formDialogCopySource.value = detailContract.value
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 详情抽屉 - 编辑 */
|
||||
const handleDetailEdit = async () => {
|
||||
if (!detailContract.value?.id) return
|
||||
@@ -1274,7 +1312,8 @@ const handleDetailEdit = async () => {
|
||||
}
|
||||
}
|
||||
visible.value = false
|
||||
formDialogEditId.value = detailContract.value.id
|
||||
resetFormDialogState()
|
||||
formDialogEditId.value = String(detailContract.value.id)
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -1874,7 +1913,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
${pricingCells}
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.packageSpec || ''}</td>
|
||||
@@ -2252,6 +2291,8 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
return unitLabel || unitValue || '';
|
||||
};
|
||||
|
||||
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
|
||||
|
||||
// 构建产品明细表格行(使用 0.3px 边框)
|
||||
const productRows =
|
||||
contractData.products
|
||||
@@ -2268,7 +2309,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
|
||||
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
|
||||
|
||||
@@ -824,7 +824,8 @@ import {
|
||||
updateContract,
|
||||
type ContractForm,
|
||||
type ContractProductForm,
|
||||
type ContractProductPriceTierForm
|
||||
type ContractProductPriceTierForm,
|
||||
type ContractVO
|
||||
} from '@/api/crm/contract/index'
|
||||
import { isFrameworkContractType, isPurchaseLikeContractType } from '../contractType'
|
||||
import {
|
||||
@@ -936,6 +937,8 @@ type ExtendedContractProductForm = ContractProductForm & {
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
editId: string | null
|
||||
/** 复制来源合同(列表选中行,含 products) */
|
||||
copySource?: ContractVO | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -953,7 +956,9 @@ const userStore = useUserStore()
|
||||
|
||||
// 表单标题
|
||||
const formTitle = computed(() => {
|
||||
return form.value.id ? '编辑合同' : '新增合同'
|
||||
if (props.editId) return '编辑合同'
|
||||
if (props.copySource) return '复制合同'
|
||||
return '新增合同'
|
||||
})
|
||||
|
||||
// 合同期限日期范围
|
||||
@@ -1077,7 +1082,7 @@ const submitLoading = ref(false)
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible && props.editId) {
|
||||
if (visible && (props.editId || props.copySource)) {
|
||||
formLoading.value = true
|
||||
} else if (!visible) {
|
||||
formLoading.value = false
|
||||
@@ -1346,7 +1351,8 @@ interface QuoteVO {
|
||||
const handleOpened = async () => {
|
||||
resetPartyExpand()
|
||||
const isEdit = !!props.editId
|
||||
if (isEdit) {
|
||||
const isCopy = !!props.copySource
|
||||
if (isEdit || isCopy) {
|
||||
formLoading.value = true
|
||||
}
|
||||
try {
|
||||
@@ -1354,23 +1360,15 @@ const handleOpened = async () => {
|
||||
|
||||
if (props.editId) {
|
||||
await loadContractDetail(props.editId)
|
||||
} else if (props.copySource) {
|
||||
await loadContractForCopy(props.copySource)
|
||||
} else {
|
||||
resetForm()
|
||||
form.value.signDate = new Date().toISOString().split('T')[0]
|
||||
handleSignDateChange(form.value.signDate)
|
||||
applyDefaultOwnerUser()
|
||||
|
||||
// 设置默认供方主体为"SX"
|
||||
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
|
||||
if (defaultSupplier) {
|
||||
form.value.supplierId = defaultSupplier.id
|
||||
await handleSupplierChange(defaultSupplier.id)
|
||||
}
|
||||
await initNewContractForm()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('合同表单初始化失败:', error)
|
||||
$modal.msgError(error?.message || '加载合同数据失败')
|
||||
if (isEdit) {
|
||||
if (isEdit || isCopy) {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
} finally {
|
||||
@@ -1378,6 +1376,20 @@ const handleOpened = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 新增合同表单默认值 */
|
||||
const initNewContractForm = async () => {
|
||||
resetForm()
|
||||
form.value.signDate = new Date().toISOString().split('T')[0]
|
||||
handleSignDateChange(form.value.signDate)
|
||||
applyDefaultOwnerUser()
|
||||
|
||||
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
|
||||
if (defaultSupplier) {
|
||||
form.value.supplierId = defaultSupplier.id
|
||||
await handleSupplierChange(defaultSupplier.id)
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
@@ -1446,6 +1458,104 @@ const resetForm = () => {
|
||||
selectedProducts.value = []
|
||||
}
|
||||
|
||||
/** 将源合同产品明细映射为表单行(复制时清除主键及数量/单价/备注) */
|
||||
const mapContractProductsFromSource = (products: any[], forCopy = false) =>
|
||||
products.map((product: any) => {
|
||||
const mapped: ExtendedContractProductForm = {
|
||||
...product,
|
||||
id: forCopy ? undefined : product.id,
|
||||
contractId: forCopy ? undefined : product.contractId,
|
||||
productModel: product.model || product.productModel || '',
|
||||
isToxic: Number(product.isToxic || 0),
|
||||
isExplosive: Number(product.isExplosive || 0),
|
||||
brandMode: product.brandMode || undefined,
|
||||
brandId: product.brandId || undefined,
|
||||
brandName: product.brandName || undefined,
|
||||
brandOptions: [],
|
||||
priceTiers: (product.priceTiers || []).map((t: any) => {
|
||||
let ids: string[] = []
|
||||
if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
|
||||
ids = t.letterOssIds.map((id: any) => String(id))
|
||||
} else if (t.letterOssId != null && t.letterOssId !== '') {
|
||||
ids = [String(t.letterOssId)]
|
||||
}
|
||||
return {
|
||||
...t,
|
||||
id: forCopy ? undefined : t.id,
|
||||
contractId: forCopy ? undefined : t.contractId,
|
||||
contractProductId: forCopy ? undefined : t.contractProductId,
|
||||
letterOssIds: ids,
|
||||
...(forCopy ? { price: undefined } : {})
|
||||
}
|
||||
}),
|
||||
taxRate: product.taxRate || 6,
|
||||
taxAmount: forCopy ? 0 : (product.taxAmount ?? 0),
|
||||
packageSpec: product.packageSpec || '',
|
||||
deliveryMethod: product.deliveryMethod || ''
|
||||
}
|
||||
if (forCopy) {
|
||||
mapped.quantity = 0
|
||||
mapped.price = 0
|
||||
mapped.unitPrice = 0
|
||||
mapped.subtotal = 0
|
||||
mapped.remark = ''
|
||||
}
|
||||
return mapped
|
||||
})
|
||||
|
||||
/** 写入产品明细并刷新金额 */
|
||||
const applyContractProductsToForm = async (products: ExtendedContractProductForm[], contractType: string) => {
|
||||
form.value.products = products
|
||||
await enrichContractProductBrandMeta(form.value.products)
|
||||
|
||||
const isFw = isFrameworkContractType(contractType)
|
||||
form.value.products.forEach((product: any) => {
|
||||
normalizeFrameworkProductPricing(product, isFw)
|
||||
if (!isFw || !isFrameworkPriceMarket(product)) {
|
||||
calculateProductAmount(product)
|
||||
}
|
||||
})
|
||||
if (isFw) {
|
||||
form.value.amount = 0
|
||||
} else {
|
||||
calculateTotalAmount()
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制合同:仅带入产品明细,客户等信息由用户重新填写 */
|
||||
const loadContractForCopy = async (source: ContractVO) => {
|
||||
await initNewContractForm()
|
||||
|
||||
const contractType = source.contractType || 'sale'
|
||||
form.value.contractType = contractType
|
||||
|
||||
let products = Array.isArray(source.products) ? source.products : []
|
||||
|
||||
// 列表 includeProducts 不含价阶;框架合同尝试拉详情补全,失败则回退列表明细
|
||||
if (isFrameworkContractType(contractType)) {
|
||||
try {
|
||||
const res = await getContract(source.id)
|
||||
if (res.data?.products?.length) {
|
||||
products = res.data.products
|
||||
form.value.contractType = res.data.contractType || contractType
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('框架合同复制:加载详情价阶失败,使用列表产品明细', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!products.length) {
|
||||
const res = await getContract(source.id)
|
||||
products = res.data?.products || []
|
||||
form.value.contractType = res.data?.contractType || contractType
|
||||
}
|
||||
|
||||
if (products.length) {
|
||||
const mapped = mapContractProductsFromSource(products, true)
|
||||
await applyContractProductsToForm(mapped, form.value.contractType)
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载合同详情 */
|
||||
const loadContractDetail = async (id: number | string) => {
|
||||
const res = await getContract(id)
|
||||
@@ -1492,47 +1602,8 @@ const loadContractDetail = async (id: number | string) => {
|
||||
}
|
||||
|
||||
if (contractData.products && Array.isArray(contractData.products)) {
|
||||
form.value.products = contractData.products.map((product: any) => ({
|
||||
...product,
|
||||
productModel: product.model || '',
|
||||
isToxic: Number(product.isToxic || 0),
|
||||
isExplosive: Number(product.isExplosive || 0),
|
||||
brandMode: product.brandMode || undefined,
|
||||
brandId: product.brandId || undefined,
|
||||
brandName: product.brandName || undefined,
|
||||
brandOptions: [],
|
||||
priceTiers: (product.priceTiers || []).map((t: any) => {
|
||||
let ids: string[] = []
|
||||
if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
|
||||
ids = t.letterOssIds.map((id: any) => String(id))
|
||||
} else if (t.letterOssId != null && t.letterOssId !== '') {
|
||||
ids = [String(t.letterOssId)]
|
||||
}
|
||||
return {
|
||||
...t,
|
||||
letterOssIds: ids
|
||||
}
|
||||
}),
|
||||
taxRate: product.taxRate || 6,
|
||||
taxAmount: product.taxAmount ?? 0,
|
||||
packageSpec: product.packageSpec || '',
|
||||
deliveryMethod: product.deliveryMethod || ''
|
||||
}))
|
||||
|
||||
await enrichContractProductBrandMeta(form.value.products)
|
||||
|
||||
const isFw = isFrameworkContractType(contractData.contractType)
|
||||
form.value.products.forEach((product: any) => {
|
||||
normalizeFrameworkProductPricing(product, isFw)
|
||||
if (!isFw || !isFrameworkPriceMarket(product)) {
|
||||
calculateProductAmount(product)
|
||||
}
|
||||
})
|
||||
if (isFw) {
|
||||
form.value.amount = 0
|
||||
} else {
|
||||
calculateTotalAmount()
|
||||
}
|
||||
const products = mapContractProductsFromSource(contractData.products)
|
||||
await applyContractProductsToForm(products, contractData.contractType)
|
||||
}
|
||||
|
||||
if (form.value.startDate && form.value.endDate) {
|
||||
|
||||
@@ -1098,7 +1098,11 @@ import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
|
||||
import { getOpportunity } from '@/api/crm/opportunity';
|
||||
import { getQuote } from '@/api/crm/quote';
|
||||
import { isFrameworkContractType, isContractEditableStatus, getContractReeditConfirmMessage } from './contractType';
|
||||
import { displayFrameworkMoney, displayFrameworkQuantity } from './frameworkProductPricing';
|
||||
import {
|
||||
displayFrameworkMoney,
|
||||
displayFrameworkQuantity,
|
||||
formatFrameworkQuantityForPrint
|
||||
} from './frameworkProductPricing';
|
||||
import {
|
||||
parseSettlementClauseDrafts,
|
||||
resolveSettlementClauseContent
|
||||
@@ -1594,6 +1598,8 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
return dictStore.getLabel('crm_product_unit', unitValue) || unitValue || '';
|
||||
};
|
||||
|
||||
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
|
||||
|
||||
// 构建产品明细表格行
|
||||
const productRows =
|
||||
contractData.products
|
||||
@@ -1610,7 +1616,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
|
||||
@@ -1699,7 +1705,6 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
|
||||
const totalWithExtrasChinese = numberToChinese(totalWithExtras);
|
||||
const freightText = freightAmount > 0 ? `¥${formatNumber(freightAmount)}` : '-';
|
||||
const barrelDepositText = barrelDepositAmount > 0 ? `¥${formatNumber(barrelDepositAmount)}` : '-';
|
||||
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
|
||||
const totalWithExtrasText = isFrameworkContract
|
||||
? '随行就市'
|
||||
: `${totalWithExtrasChinese}(¥${formatNumber(totalWithExtras)})`;
|
||||
@@ -1962,7 +1967,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.productName || ''}</td>
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
|
||||
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
|
||||
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
|
||||
|
||||
@@ -18,6 +18,8 @@ export type FrameworkPriceMode =
|
||||
(typeof FRAMEWORK_PRICE_MODE)[keyof typeof FRAMEWORK_PRICE_MODE]
|
||||
|
||||
export const FRAMEWORK_QUANTITY_LABEL = '按客户需求'
|
||||
/** 打印/PDF 富文本中「按客户需求」对应的用量文案 */
|
||||
export const FRAMEWORK_QUANTITY_PRINT_LABEL = '按实际到货数量'
|
||||
export const FRAMEWORK_PRICE_LABEL = '随行就市'
|
||||
|
||||
export type FrameworkPricingProduct = {
|
||||
@@ -160,6 +162,20 @@ export function displayFrameworkQuantity(product: FrameworkPricingProduct, isFra
|
||||
return formatFrameworkQuantityDisplay(normalizeFrameworkProductPricing({ ...product }, true))
|
||||
}
|
||||
|
||||
/** 打印/PDF 富文本用量列:按客户需求时显示「按实际到货数量」,否则与原先一致输出数量 */
|
||||
export function formatFrameworkQuantityForPrint(
|
||||
product: FrameworkPricingProduct,
|
||||
isFramework: boolean
|
||||
): string {
|
||||
if (isFramework) {
|
||||
const normalized = normalizeFrameworkProductPricing({ ...product }, true)
|
||||
if (isFrameworkQuantityCustomerDemand(normalized)) {
|
||||
return FRAMEWORK_QUANTITY_PRINT_LABEL
|
||||
}
|
||||
}
|
||||
return String(product.quantity || 0)
|
||||
}
|
||||
|
||||
export function displayFrameworkMoney(
|
||||
product: FrameworkPricingProduct,
|
||||
field: 'price' | 'subtotal' | 'taxAmount',
|
||||
|
||||
@@ -320,7 +320,8 @@ import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
fetchCustomerDetail,
|
||||
fetchCreateCustomer,
|
||||
fetchUpdateCustomer
|
||||
fetchUpdateCustomer,
|
||||
fetchCheckDuplicate
|
||||
} from '@/api/crm/customer'
|
||||
import type { CustomerForm as CustomerFormType } from '@/api/crm/customerTypes'
|
||||
import { addInvoiceInfo } from '@/api/crm/invoiceInfo'
|
||||
@@ -377,8 +378,40 @@ function unwrapApiData<T>(res: T | { data?: T }): T {
|
||||
}
|
||||
|
||||
// 表单校验规则
|
||||
const validateCustomerNameUnique = (
|
||||
_rule: unknown,
|
||||
value: string,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
const name = (value || '').trim()
|
||||
if (!name) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
fetchCheckDuplicate({
|
||||
customerName: name,
|
||||
excludeId: form.value.id != null ? String(form.value.id) : undefined
|
||||
})
|
||||
.then((res) => {
|
||||
const data = unwrapApiData(res as Record<string, unknown> | { data?: Record<string, unknown> })
|
||||
if (data?.isDuplicate && Array.isArray(data.duplicates) && data.duplicates.length) {
|
||||
const dup = data.duplicates[0] as Record<string, unknown>
|
||||
const customerNo = (dup.customerNo || dup.customer_no || '') as string
|
||||
callback(
|
||||
new Error(customerNo ? `客户名称已存在(编号:${customerNo})` : '客户名称已存在')
|
||||
)
|
||||
return
|
||||
}
|
||||
callback()
|
||||
})
|
||||
.catch(() => callback())
|
||||
}
|
||||
|
||||
const rules = ref<Record<string, FormItemRule[]>>({
|
||||
customerName: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }],
|
||||
customerName: [
|
||||
{ required: true, message: '客户名称不能为空', trigger: 'blur' },
|
||||
{ validator: validateCustomerNameUnique, trigger: 'blur' }
|
||||
],
|
||||
shortName: [{ required: true, message: '客户简称不能为空', trigger: 'blur' }],
|
||||
customerType: [{ required: true, message: '客户类型不能为空', trigger: 'change' }],
|
||||
source: [{ required: true, message: '客户来源不能为空', trigger: 'change' }],
|
||||
@@ -592,6 +625,7 @@ function buildSubmitPayload() {
|
||||
delete payload.businessLicenseImg
|
||||
// 显式传空串:后端按「重新生成」处理;有值则手改入库
|
||||
payload.namePinyinAbbr = (form.value.namePinyinAbbr || '').trim()
|
||||
payload.customerName = (form.value.customerName || '').trim()
|
||||
return payload
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('id')"
|
||||
prop="id"
|
||||
label="ID"
|
||||
label="出入库单ID"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
@@ -569,7 +569,7 @@
|
||||
const loading = ref(false)
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'id', label: 'ID', defaultVisible: false },
|
||||
{ prop: 'id', label: '出入库单ID' },
|
||||
{ prop: 'planNo', label: '计划号' },
|
||||
{ prop: 'date', label: '日期' },
|
||||
{ prop: 'partnerCompany', label: '往来单位' },
|
||||
@@ -595,7 +595,7 @@
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('db-inventory-list-columns', listColumnDefs)
|
||||
} = usePersistedListColumns('db-inventory-list-columns-v2', listColumnDefs)
|
||||
|
||||
const inventoryExportColumns = computed(() => {
|
||||
const cols = [...visibleColumns.value]
|
||||
|
||||
@@ -126,6 +126,19 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="60" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('inboundOutboundId')"
|
||||
prop="inboundOutboundId"
|
||||
label="出入库单ID"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.inboundOutboundId || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('inventoryDate')"
|
||||
prop="inventoryDate"
|
||||
@@ -559,6 +572,7 @@
|
||||
const router = useRouter()
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'inboundOutboundId', label: '出入库单ID' },
|
||||
{ prop: 'inventoryDate', label: '日期' },
|
||||
{ prop: 'planNo', label: '计划单号' },
|
||||
{ prop: 'vehicleNo', label: '车牌号' },
|
||||
@@ -584,7 +598,7 @@
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('db-weighing-list-columns', listColumnDefs)
|
||||
} = usePersistedListColumns('db-weighing-list-columns-v2', listColumnDefs)
|
||||
|
||||
const weighingExportColumns = computed(() => {
|
||||
const cols = [...visibleColumns.value]
|
||||
@@ -605,6 +619,9 @@
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('inboundOutboundId')) {
|
||||
cols.push({ key: 'inboundOutboundId', interactive: true, prop: 'inboundOutboundId' })
|
||||
}
|
||||
if (isColumnVisible('inventoryDate')) {
|
||||
cols.push({ key: 'inventoryDate', interactive: true, prop: 'inventoryDate' })
|
||||
}
|
||||
@@ -613,7 +630,9 @@
|
||||
}
|
||||
cols.push({ key: 'batchNo', interactive: true, prop: 'batchNo' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'inventoryDate' || def.prop === 'planNo') continue
|
||||
if (def.prop === 'inventoryDate' || def.prop === 'planNo' || def.prop === 'inboundOutboundId') {
|
||||
continue
|
||||
}
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
|
||||
@@ -1478,20 +1478,20 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
.chip.am {
|
||||
color: #b45309;
|
||||
background: #fffbeb;
|
||||
color: #fff;
|
||||
background: #d97706;
|
||||
}
|
||||
.chip.pm {
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
color: #fff;
|
||||
background: #2563eb;
|
||||
}
|
||||
.chip.duty {
|
||||
color: #b91c1c;
|
||||
background: #fef2f2;
|
||||
color: #fff;
|
||||
background: #dc2626;
|
||||
}
|
||||
.chip.other {
|
||||
color: #475569;
|
||||
background: #f1f5f9;
|
||||
color: #fff;
|
||||
background: #475569;
|
||||
}
|
||||
.chip.empty {
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
|
||||
@@ -102,6 +102,27 @@
|
||||
:selected-ids="selectedRows.map((r) => r.id)"
|
||||
filename="计划归档.xlsx"
|
||||
/>
|
||||
<el-dropdown trigger="click" @command="handleBatchErpCommand">
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:loading="erpBatchLoading"
|
||||
>
|
||||
<el-icon><DocumentChecked /></el-icon>
|
||||
ERP录入
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="entered" :disabled="erpBatchLoading">
|
||||
已录入ERP
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="not_entered" :disabled="erpBatchLoading">
|
||||
未录入ERP
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
@@ -229,10 +250,8 @@
|
||||
<template #default="{ row }">
|
||||
<div class="pl-plan-archive-erp-cell" @click.stop @mousedown.stop>
|
||||
<el-checkbox
|
||||
class="pl-plan-archive-erp-checkbox"
|
||||
:model-value="Number(row.erpEntered) === 1"
|
||||
:disabled="row.followStatus === 'cancelled' || erpUpdatingId === row.id"
|
||||
@click.stop
|
||||
@change="(checked) => handleErpEnteredChange(row, Boolean(checked))"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -431,6 +450,8 @@ import ListColumnStatsRow from '@/components/business/list-column-stats-row/inde
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
ArrowDown,
|
||||
DocumentChecked,
|
||||
FolderChecked,
|
||||
RefreshLeft,
|
||||
Search
|
||||
@@ -472,7 +493,7 @@ const viewPlanId = ref('')
|
||||
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
||||
const archiveDialogVisible = ref(false)
|
||||
const archivePlanId = ref('')
|
||||
const erpUpdatingId = ref('')
|
||||
const erpBatchLoading = ref(false)
|
||||
const planList = ref<Plan[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
@@ -765,25 +786,49 @@ function handleToolbarArchive() {
|
||||
handleArchive(selectedRows.value[0])
|
||||
}
|
||||
|
||||
async function handleErpEnteredChange(row: Plan, entered: boolean) {
|
||||
if (row.followStatus === 'cancelled') return
|
||||
const previous = Number(row.erpEntered) === 1
|
||||
if (previous === entered) return
|
||||
async function handleBatchErpCommand(command: 'entered' | 'not_entered') {
|
||||
const entered = command === 'entered'
|
||||
const eligibleRows = selectedRows.value.filter((row) => row.followStatus !== 'cancelled')
|
||||
if (eligibleRows.length === 0) {
|
||||
ElMessage.warning('请选择未作废的计划')
|
||||
return
|
||||
}
|
||||
|
||||
erpUpdatingId.value = row.id
|
||||
row.erpEntered = entered ? 1 : 0
|
||||
const targets = eligibleRows.filter((row) => (Number(row.erpEntered) === 1) !== entered)
|
||||
if (targets.length === 0) {
|
||||
ElMessage.info(entered ? '所选计划均已录入 ERP' : '所选计划均未录入 ERP')
|
||||
return
|
||||
}
|
||||
|
||||
erpBatchLoading.value = true
|
||||
try {
|
||||
const res = await planApi.setErpEntered(row.id, entered)
|
||||
const data = pickBxData<Plan>(res)
|
||||
if (data?.id) {
|
||||
row.erpEntered = data.erpEntered ?? (entered ? 1 : 0)
|
||||
const results = await Promise.allSettled(
|
||||
targets.map((row) => planApi.setErpEntered(row.id, entered))
|
||||
)
|
||||
let successCount = 0
|
||||
results.forEach((result, index) => {
|
||||
const row = targets[index]
|
||||
if (result.status === 'fulfilled') {
|
||||
const data = pickBxData<Plan>(result.value)
|
||||
row.erpEntered = data?.erpEntered ?? (entered ? 1 : 0)
|
||||
successCount += 1
|
||||
return
|
||||
}
|
||||
const message =
|
||||
result.reason instanceof Error ? result.reason.message : '更新 ERP 状态失败'
|
||||
ElMessage.error(
|
||||
`${row.planNo || row.id}:${message.includes('失败') ? message : `更新 ERP 状态失败:${message}`}`
|
||||
)
|
||||
})
|
||||
|
||||
if (successCount > 0) {
|
||||
ElMessage.success(
|
||||
entered ? `已将 ${successCount} 条计划标记为已录入 ERP` : `已将 ${successCount} 条计划标记为未录入 ERP`
|
||||
)
|
||||
await refreshViewIfOpen()
|
||||
}
|
||||
} catch (error) {
|
||||
row.erpEntered = previous ? 1 : 0
|
||||
const message = error instanceof Error ? error.message : '更新 ERP 状态失败'
|
||||
ElMessage.error(message.includes('失败') ? message : `更新 ERP 状态失败:${message}`)
|
||||
} finally {
|
||||
erpUpdatingId.value = ''
|
||||
erpBatchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,4 +867,9 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pl-plan-archive-erp-checkbox {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
@@ -173,15 +173,6 @@
|
||||
>
|
||||
<el-icon><DocumentCopy /></el-icon>三方人车快速创建
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canForceFollowStatus"
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedPlan"
|
||||
@click="openForceFollowStatusDialog"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>强制改状态
|
||||
</el-button>
|
||||
|
||||
<ListExportDropdown
|
||||
:export-fn="planApi.export"
|
||||
:get-filter-params="getPlanExportFilterParams"
|
||||
@@ -571,6 +562,13 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button
|
||||
v-if="canForceFollowStatus"
|
||||
class="btn-ghost"
|
||||
@click="openForceFollowStatusDialog"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>强制改状态
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canVoidPlan(viewingPlan)"
|
||||
class="btn-ghost"
|
||||
@@ -614,27 +612,27 @@
|
||||
>
|
||||
<el-form label-position="top" class="form-grid-2">
|
||||
<el-form-item label="计划编号" style="margin-bottom: 0">
|
||||
<span>{{ selectedPlan?.planNo || '—' }}</span>
|
||||
<span>{{ viewingPlan?.planNo || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="单据类型" style="margin-bottom: 0">
|
||||
<span>{{ getPlanTypeLabel(selectedPlan?.planType) }}</span>
|
||||
<span>{{ getPlanTypeLabel(viewingPlan?.planType) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="供方" style="margin-bottom: 0">
|
||||
<span>{{ selectedPlan?.supplierName || '—' }}</span>
|
||||
<span>{{ viewingPlan?.supplierName || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="需方" style="margin-bottom: 0">
|
||||
<span>{{ selectedPlan?.customerName || '—' }}</span>
|
||||
<span>{{ viewingPlan?.customerName || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品信息" class="form-col-span-2" style="margin-bottom: 0">
|
||||
<PlPlanListProductCell :products="selectedPlan?.products || []" />
|
||||
<PlPlanListProductCell :products="viewingPlan?.products || []" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前跟进状态" style="margin-bottom: 0">
|
||||
<el-tag
|
||||
v-if="selectedPlan"
|
||||
:type="getPlPlanFollowStatusTagType(selectedPlan.followStatus)"
|
||||
v-if="viewingPlan"
|
||||
:type="getPlPlanFollowStatusTagType(viewingPlan.followStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ getPlPlanFollowStatusLabel(selectedPlan.followStatus) }}
|
||||
{{ getPlPlanFollowStatusLabel(viewingPlan.followStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</el-form-item>
|
||||
@@ -1284,8 +1282,8 @@ function handleToolbarVoid() {
|
||||
}
|
||||
|
||||
function openForceFollowStatusDialog() {
|
||||
if (!selectedPlan.value) return
|
||||
forceFollowStatusValue.value = selectedPlan.value.followStatus || ''
|
||||
if (!viewingPlan.value) return
|
||||
forceFollowStatusValue.value = viewingPlan.value.followStatus || ''
|
||||
forceFollowDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -1294,22 +1292,24 @@ function resetForceFollowDialog() {
|
||||
}
|
||||
|
||||
async function submitForceFollowStatus() {
|
||||
if (!selectedPlan.value) return
|
||||
if (!viewingPlan.value) return
|
||||
const next = (forceFollowStatusValue.value || '').trim()
|
||||
if (!next) {
|
||||
ElMessage.warning('请选择目标跟进状态')
|
||||
return
|
||||
}
|
||||
if (next === selectedPlan.value.followStatus) {
|
||||
if (next === viewingPlan.value.followStatus) {
|
||||
ElMessage.warning('目标状态与当前状态相同')
|
||||
return
|
||||
}
|
||||
forceFollowLoading.value = true
|
||||
try {
|
||||
await planApi.forceFollowStatus(selectedPlan.value.id, next)
|
||||
await planApi.forceFollowStatus(viewingPlan.value.id, next)
|
||||
ElMessage.success('跟进状态已更新')
|
||||
forceFollowDialogVisible.value = false
|
||||
fetchData()
|
||||
syncViewingPlan({ ...viewingPlan.value, followStatus: next })
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : '修改失败'
|
||||
ElMessage.error(message)
|
||||
|
||||
Reference in new issue
Block a user