This commit is contained in:
zlwu committed 2026-08-27 19:38:01 +08:00
1 parent 4611dbc2b2
commit ebf006eeff
46 files changed
+36802 -20974

No files matched your search

@@ -159,7 +159,7 @@
</template>
</el-dialog>
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" hide-product-price />
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
</template>
<script setup lang="ts">
@@ -220,7 +220,6 @@ const contractList = ref<ContractVO[]>([])
const currentRow = ref<ContractVO | null>(null)
const selectedId = ref('')
const planCountMap = ref<Record<string, number>>({})
const contractDetailDialogActive = ref(false)
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
const queryParams = reactive<ContractQuery>({
@@ -265,15 +264,32 @@ function formatContractProductLine(product: ContractProductForm) {
)
}
async function waitForContractDetailDialog() {
if (contractDetailRef.value) return contractDetailRef.value
await nextTick()
if (contractDetailRef.value) return contractDetailRef.value
return new Promise<NonNullable<typeof contractDetailRef.value>>((resolve) => {
const stop = watch(
contractDetailRef,
(val) => {
if (val) {
stop()
resolve(val)
}
},
{ flush: 'post' }
)
})
}
async function handleViewContract(row: ContractVO) {
const contractId = row?.id
if (!contractId) {
ElMessage.warning('合同信息无效')
return
}
contractDetailDialogActive.value = true
await nextTick()
contractDetailRef.value?.open(contractId)
const dialog = await waitForContractDetailDialog()
await dialog.open(contractId)
}
function isContractSelectable(row: ContractVO): boolean {
File diff suppressed because it is too large. Load diff
@@ -147,11 +147,7 @@
@confirm-multiple="handleConfirmMultiple"
/>
<ContractDetailDialog
v-if="contractDetailDialogActive"
ref="contractDetailRef"
hide-product-price
/>
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
</div>
</template>
@@ -202,7 +198,6 @@ const emit = defineEmits<{
const selectorVisible = ref(false)
const detailRows = ref<ContractVO[]>([])
const loadingDetails = ref(false)
const contractDetailDialogActive = ref(false)
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
const expandedContractIds = ref<Set<string>>(new Set())
@@ -344,6 +339,24 @@ function removeContract(id: string) {
emitLinksFromDetails(next)
}
async function waitForContractDetailDialog() {
if (contractDetailRef.value) return contractDetailRef.value
await nextTick()
if (contractDetailRef.value) return contractDetailRef.value
return new Promise<NonNullable<typeof contractDetailRef.value>>((resolve) => {
const stop = watch(
contractDetailRef,
(val) => {
if (val) {
stop()
resolve(val)
}
},
{ flush: 'post' }
)
})
}
async function handleViewContract(contractId: string) {
const id = String(contractId || '').trim()
if (!id) {
@@ -351,9 +364,8 @@ async function handleViewContract(contractId: string) {
return
}
emit('view', id)
contractDetailDialogActive.value = true
await nextTick()
contractDetailRef.value?.open(id)
const dialog = await waitForContractDetailDialog()
await dialog.open(id)
}
defineExpose({
@@ -1,8 +1,20 @@
<template>
<div class="panel dialog-list-panel pl-plan-product-table-wrap">
<div class="list-body list-wrap dialog-list-body-only">
<ListTable table-key="business-pl-plan-form-dialog-pl-plan-form-product-table" :data="products" stripe class="list-table pl-plan-product-table" empty-text="暂无产品">
<el-table-column label="序号" type="index" width="60" align="center" header-align="center" />
<ListTable
table-key="business-pl-plan-form-dialog-pl-plan-form-product-table"
:data="products"
stripe
class="list-table pl-plan-product-table"
empty-text="暂无产品"
>
<el-table-column
label="序号"
type="index"
width="60"
align="center"
header-align="center"
/>
<el-table-column label="产品名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<PlProductNameHazardCell
@@ -10,7 +22,13 @@
/>
</template>
</el-table-column>
<el-table-column label="别名" min-width="140" align="center" show-overflow-tooltip>
<el-table-column
v-if="!archiveCompact"
label="别名"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<div v-if="editableAliasBrand" class="line-meta-cell">
<el-select
@@ -44,7 +62,13 @@
<span v-else>{{ row.aliasName || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="品牌" min-width="140" align="center" show-overflow-tooltip>
<el-table-column
v-if="!archiveCompact"
label="品牌"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<div v-if="editableAliasBrand" class="line-meta-cell">
<el-select
@@ -78,7 +102,12 @@
<span v-else>{{ formatBrandConstraintLabel(row.brandMode, row.brandName) }}</span>
</template>
</el-table-column>
<el-table-column label="型号/规格" min-width="120" show-overflow-tooltip>
<el-table-column
v-if="!archiveCompact"
label="型号/规格"
min-width="120"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.model || row.productModel || '—' }}
</template>
@@ -217,17 +246,18 @@
{{ formatPlanProductIoAmount(row.outboundQty, row.outboundPrice) }}
</template>
</el-table-column>
<el-table-column
v-if="showBrandQty"
label="品牌数量"
min-width="180"
show-overflow-tooltip
>
<el-table-column v-if="showBrandQty" label="品牌数量" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
{{ formatBrandQtySummary(row.brandQtyRows) || '—' }}
</template>
</el-table-column>
<el-table-column prop="packageSpec" label="包装规格" min-width="150" align="center">
<el-table-column
v-if="!archiveCompact"
prop="packageSpec"
label="包装规格"
min-width="150"
align="center"
>
<template #default="{ row }">
<DictSelect
v-if="editablePackageDelivery"
@@ -243,7 +273,7 @@
</template>
</template>
</el-table-column>
<el-table-column label="送货方式" min-width="150" align="center">
<el-table-column v-if="!archiveCompact" label="送货方式" min-width="150" align="center">
<template #default="{ row }">
<DictSelect
v-if="editablePackageDelivery"
@@ -263,7 +293,11 @@
</el-table-column>
<el-table-column v-if="editableRemark" label="备注" min-width="140">
<template #default="{ row }">
<el-input v-model="row.remark" class="pl-plan-product-cell-control" placeholder="备注" />
<el-input
v-model="row.remark"
class="pl-plan-product-cell-control"
placeholder="备注"
/>
</template>
</el-table-column>
<el-table-column v-else prop="remark" label="备注" min-width="120" show-overflow-tooltip />
@@ -308,172 +342,166 @@
</template>
<script setup lang="ts">
import { Delete, Plus } from '@element-plus/icons-vue'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan'
import {
applyBrandSelectChange,
applyManagedProductMetaToLines,
formatBrandConstraintLabel,
resolveLineProductName
} from '@/utils/crm-product-brand'
import { formatBrandQtySummary } from '@/utils/pl-display'
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
import DictTag from '@/components/DictTag/index.vue'
import DictSelect from '@/components/DictSelect/index.vue'
import { useDictStore } from '@/store/modules/dict'
import { Delete, Plus } from '@element-plus/icons-vue'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan'
import {
applyBrandSelectChange,
applyManagedProductMetaToLines,
formatBrandConstraintLabel,
resolveLineProductName
} from '@/utils/crm-product-brand'
import { formatBrandQtySummary } from '@/utils/pl-display'
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
import DictTag from '@/components/DictTag/index.vue'
import DictSelect from '@/components/DictSelect/index.vue'
import { useDictStore } from '@/store/modules/dict'
const props = defineProps<{
products: PlanFormProductRow[]
showDelete?: boolean
showSelect?: boolean
/** 已加入运输计划的产品 ID,用于禁用合同预览中的「选择」按钮 */
selectedProductIds?: string[]
editableRemark?: boolean
editableQty?: boolean
/** 产品信息区:别名 / 品牌可改 */
editableAliasBrand?: boolean
/** 单位 / 包装规格 / 送货方式可下拉编辑(对齐报价单) */
editablePackageDelivery?: boolean
/** 新增/编辑弹窗默认不展示实际数量 */
showActualQty?: boolean
/** 展示归档品牌数量摘要(如:君正 12 / 红四方 13) */
showBrandQty?: boolean
/** 仅实际数量可编辑,计划数量只读(归档弹窗) */
editableActualQtyOnly?: boolean
/** 展示入库/出库数量列(不含价格) */
showInboundOutboundQty?: boolean
/** 展示入库/出库数量、单价与计算金额列 */
showArchiveIoFields?: boolean
/** 入库/出库数量与价格可编辑(归档弹窗) */
editableArchiveIoFields?: boolean
/** 归档:仅展示/编辑入库列(装货段) */
archiveIoInboundOnly?: boolean
/** 归档:仅展示/编辑出库列(送货段) */
archiveIoOutboundOnly?: boolean
/** 段次归档:入库/出库列统一展示为实际数量、单价、金额 */
archiveSegmentLabels?: boolean
}>()
const props = defineProps<{
products: PlanFormProductRow[]
showDelete?: boolean
showSelect?: boolean
/** 已加入运输计划的产品 ID,用于禁用合同预览中的「选择」按钮 */
selectedProductIds?: string[]
editableRemark?: boolean
editableQty?: boolean
/** 产品信息区:别名 / 品牌可改 */
editableAliasBrand?: boolean
/** 单位 / 包装规格 / 送货方式可下拉编辑(对齐报价单) */
editablePackageDelivery?: boolean
/** 新增/编辑弹窗默认不展示实际数量 */
showActualQty?: boolean
/** 展示归档品牌数量摘要(如:君正 12 / 红四方 13) */
showBrandQty?: boolean
/** 仅实际数量可编辑,计划数量只读(归档弹窗) */
editableActualQtyOnly?: boolean
/** 展示入库/出库数量列(不含价格) */
showInboundOutboundQty?: boolean
/** 展示入库/出库数量、单价与计算金额列 */
showArchiveIoFields?: boolean
/** 入库/出库数量与价格可编辑(归档弹窗) */
editableArchiveIoFields?: boolean
/** 归档:仅展示/编辑入库列(装货段) */
archiveIoInboundOnly?: boolean
/** 归档:仅展示/编辑出库列(送货段) */
archiveIoOutboundOnly?: boolean
/** 段次归档:入库/出库列统一展示为实际数量、单价、金额 */
archiveSegmentLabels?: boolean
/** 归档段表单:仅保留产品名称、单位、计划数量与归档列 */
archiveCompact?: boolean
}>()
const emit = defineEmits<{
remove: [index: number]
select: [row: PlanFormProductRow, index: number]
}>()
const emit = defineEmits<{
remove: [index: number]
select: [row: PlanFormProductRow, index: number]
}>()
const dictStore = useDictStore()
const dictStore = useDictStore()
const selectedProductIdSet = computed(
() => new Set((props.selectedProductIds || []).map(id => String(id)))
)
function getDeliveryMethodDisplay(method?: string) {
const label = dictStore.getLabel('crm_shfs', method || '')
if (label && label !== method) return label
return getDeliveryMethodLabel(method)
}
function isProductSelected(row: PlanFormProductRow) {
return selectedProductIdSet.value.has(String(row.productId))
}
/** 入库/出库金额:数量 × 单价,保留两位小数 */
function formatPlanProductIoAmount(
qty?: number | string | null,
price?: number | string | null
) {
if (qty == null || price == null || qty === '' || price === '') return '—'
const amount = Number(qty) * Number(price)
if (!Number.isFinite(amount)) return '—'
return amount.toFixed(2)
}
function handleAliasChange(row: PlanFormProductRow) {
row.productName = resolveLineProductName(row.standardProductName, row.aliasName)
}
function handleBrandChange(row: PlanFormProductRow, val: string | undefined | null) {
applyBrandSelectChange(row, val, row.brandOptions || [])
}
const aliasBrandManageVisible = ref(false)
const aliasBrandManageProductId = ref('')
const aliasBrandManageProductName = ref('')
const aliasBrandManageMode = ref<'alias' | 'brand'>('alias')
function openAliasBrandManage(row: PlanFormProductRow, mode: 'alias' | 'brand') {
if (!row.productId) return
aliasBrandManageProductId.value = String(row.productId)
aliasBrandManageProductName.value = String(row.standardProductName || row.productName || '')
aliasBrandManageMode.value = mode
aliasBrandManageVisible.value = true
}
function handleAliasBrandManaged(product: Record<string, any>) {
applyManagedProductMetaToLines(
props.products || [],
aliasBrandManageProductId.value,
product
const selectedProductIdSet = computed(
() => new Set((props.selectedProductIds || []).map((id) => String(id)))
)
const pid = aliasBrandManageProductId.value
for (const line of props.products || []) {
if (String(line.productId || '') !== pid) continue
line.productName = resolveLineProductName(line.standardProductName, line.aliasName)
function getDeliveryMethodDisplay(method?: string) {
const label = dictStore.getLabel('crm_shfs', method || '')
if (label && label !== method) return label
return getDeliveryMethodLabel(method)
}
function isProductSelected(row: PlanFormProductRow) {
return selectedProductIdSet.value.has(String(row.productId))
}
/** 入库/出库金额:数量 × 单价,保留两位小数 */
function formatPlanProductIoAmount(qty?: number | string | null, price?: number | string | null) {
if (qty == null || price == null || qty === '' || price === '') return '—'
const amount = Number(qty) * Number(price)
if (!Number.isFinite(amount)) return '—'
return amount.toFixed(2)
}
function handleAliasChange(row: PlanFormProductRow) {
row.productName = resolveLineProductName(row.standardProductName, row.aliasName)
}
function handleBrandChange(row: PlanFormProductRow, val: string | undefined | null) {
applyBrandSelectChange(row, val, row.brandOptions || [])
}
const aliasBrandManageVisible = ref(false)
const aliasBrandManageProductId = ref('')
const aliasBrandManageProductName = ref('')
const aliasBrandManageMode = ref<'alias' | 'brand'>('alias')
function openAliasBrandManage(row: PlanFormProductRow, mode: 'alias' | 'brand') {
if (!row.productId) return
aliasBrandManageProductId.value = String(row.productId)
aliasBrandManageProductName.value = String(row.standardProductName || row.productName || '')
aliasBrandManageMode.value = mode
aliasBrandManageVisible.value = true
}
function handleAliasBrandManaged(product: Record<string, any>) {
applyManagedProductMetaToLines(props.products || [], aliasBrandManageProductId.value, product)
const pid = aliasBrandManageProductId.value
for (const line of props.products || []) {
if (String(line.productId || '') !== pid) continue
line.productName = resolveLineProductName(line.standardProductName, line.aliasName)
}
}
}
</script>
<style scoped lang="scss">
.pl-plan-product-table-wrap {
margin-top: 8px;
.pl-plan-product-table-wrap {
margin-top: 8px;
/* 别名 / 品牌下拉与备注输入框同高、同字号 */
:deep(.pl-plan-product-cell-control) {
.el-input__wrapper,
.el-select__wrapper {
height: var(--el-component-custom-height, 36px);
min-height: var(--el-component-custom-height, 36px);
font-size: 14px;
/* 别名 / 品牌下拉与备注输入框同高、同字号 */
:deep(.pl-plan-product-cell-control) {
.el-input__wrapper,
.el-select__wrapper {
height: var(--el-component-custom-height, 36px);
min-height: var(--el-component-custom-height, 36px);
font-size: 14px;
}
.el-input__inner,
.el-select__selected-item,
.el-select__placeholder,
.el-select__input {
font-size: 14px;
}
}
.el-input__inner,
.el-select__selected-item,
.el-select__placeholder,
.el-select__input {
font-size: 14px;
.line-meta-cell {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.line-meta-cell__control {
flex: 1;
min-width: 0;
}
.line-meta-cell__empty {
flex: 1;
min-width: 0;
}
.line-meta-cell__btn {
flex-shrink: 0;
padding: 4px;
}
}
.line-meta-cell {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.line-meta-cell__control {
flex: 1;
min-width: 0;
}
.line-meta-cell__empty {
flex: 1;
min-width: 0;
}
.line-meta-cell__btn {
flex-shrink: 0;
padding: 4px;
}
}
</style>
<style lang="scss">
/* teleported 下拉挂到 body,需非 scoped;选项字号与输入框一致 */
.pl-plan-alias-brand-select-dropdown {
.el-select-dropdown__item {
font-size: 14px;
/* teleported 下拉挂到 body,需非 scoped;选项字号与输入框一致 */
.pl-plan-alias-brand-select-dropdown {
.el-select-dropdown__item {
font-size: 14px;
}
}
}
</style>
@@ -13,7 +13,9 @@
field === 'planQty' ||
field === 'actualQty' ||
field === 'inboundQty' ||
field === 'outboundQty',
field === 'outboundQty' ||
field === 'unitPrice' ||
field === 'amount',
'pl-plan-list-product-cell__line--center':
field === 'spec' || field === 'model' || field === 'packageSpec' || field === 'unit'
}"
@@ -64,7 +66,9 @@
</span>
</template>
<template v-else-if="field === 'name'">
<span class="pl-plan-list-product-cell__text">{{ product.productName || '—' }}</span>
<span class="pl-plan-list-product-cell__text">{{
resolveListProductName(product)
}}</span>
<el-tag
v-if="getHazardFlags(product).isToxic"
type="warning"
@@ -109,6 +113,13 @@
getPlanProductQuantity,
getPlanProductSpec
} from '@/utils/pl-display'
import {
getStopArchiveActualQty,
getStopArchiveAmount,
getStopArchiveUnitPrice,
resolveArchiveListIoColumn,
type PlPlanListDisplayRowKind
} from '@/utils/pl-plan-list-display-rows'
import { computed } from 'vue'
export type PlPlanListProductCellField =
@@ -121,6 +132,8 @@
| 'actualQty'
| 'inboundQty'
| 'outboundQty'
| 'unitPrice'
| 'amount'
| 'unit'
const props = withDefaults(
@@ -134,12 +147,18 @@
field?: PlPlanListProductCellField
/** 危化品行下方展示证书有无(按产品类型;仅整行模式) */
showCertStatus?: boolean
/** 运输计划归档列表:段次数量/入出库/单价/金额展示规则 */
archiveListMode?: boolean
stopKind?: PlPlanListDisplayRowKind
stopCustomerName?: string
}>(),
{
products: () => [],
stopProducts: () => [],
planProducts: () => [],
showCertStatus: false
showCertStatus: false,
archiveListMode: false,
stopCustomerName: ''
}
)
@@ -168,6 +187,19 @@
)
}
/** 列表「产品名称」列:只展示标准产品名,不展示别名 */
function resolveListProductName(product: PlanProduct | PlanStopProduct) {
if (useStopProducts.value) {
const stopProduct = product as PlanStopProduct
const linked = resolveLinkedPlanProduct(stopProduct)
const linkedName = (linked?.productName || '').trim()
if (linkedName) return linkedName
return stopProduct.productName || '—'
}
const planProduct = product as PlanProduct
return planProduct.productName || '—'
}
function formatStopProductLine(stopProduct: PlanStopProduct) {
const linked = resolveLinkedPlanProduct(stopProduct)
if (linked) return formatPlanProductListText(linked, getUnitLabel)
@@ -180,14 +212,25 @@
return [stopProduct.productName || '—', model, qty].filter(Boolean).join('')
}
function formatStopQty(value: unknown, unitRaw?: string) {
function formatStopQty(value: unknown, unitRaw?: string, omitUnit = false) {
if (value == null || value === '') return '—'
const num = Number(value)
if (!Number.isFinite(num)) return '—'
if (omitUnit) return String(num)
const unit = getUnitLabel(unitRaw || '') || unitRaw || ''
return unit ? `${num} ${unit}` : String(num)
}
function formatArchiveQty(value: number | null) {
if (value == null) return '—'
return String(value)
}
function formatMoneyValue(value: number | null) {
if (value == null) return '—'
return value.toFixed(2)
}
function formatProductLine(product: PlanProduct) {
return formatPlanProductListText(product, getUnitLabel)
}
@@ -196,6 +239,29 @@
if (useStopProducts.value) {
const stopProduct = product as PlanStopProduct
const linked = resolveLinkedPlanProduct(stopProduct)
const stopKind = props.stopKind || 'load'
if (props.archiveListMode) {
const ioColumn = resolveArchiveListIoColumn(stopKind, props.stopCustomerName)
const actualQty = getStopArchiveActualQty(stopProduct, stopKind)
if (props.field === 'actualQty') {
return formatArchiveQty(actualQty)
}
if (props.field === 'inboundQty') {
return ioColumn === 'inbound' ? formatArchiveQty(actualQty) : '—'
}
if (props.field === 'outboundQty') {
return ioColumn === 'outbound' ? formatArchiveQty(actualQty) : '—'
}
if (props.field === 'unitPrice') {
return formatMoneyValue(getStopArchiveUnitPrice(stopProduct, stopKind))
}
if (props.field === 'amount') {
return formatMoneyValue(getStopArchiveAmount(stopProduct, stopKind))
}
}
if (props.field === 'spec' || props.field === 'model') {
return (
stopProduct.model || linked?.model || (linked ? getPlanProductSpec(linked) : '—') || '—'
@@ -214,6 +280,10 @@
return formatStopQty(stopProduct.outboundQty, stopProduct.unit)
}
if (props.field === 'actualQty') {
const segmentKind = props.stopKind || (props.archiveListMode ? 'load' : undefined)
if (props.archiveListMode || segmentKind) {
return formatArchiveQty(getStopArchiveActualQty(stopProduct, segmentKind || 'load'))
}
return linked ? getPlanProductActualQuantity(linked) : '—'
}
if (props.field === 'unit') {
@@ -249,6 +319,9 @@
if (props.field === 'outboundQty') {
return getPlanProductOutboundQuantity(planProduct)
}
if (props.field === 'unitPrice' || props.field === 'amount') {
return '—'
}
if (props.field === 'unit') {
const unitRaw = planProduct.unit || ''
if (!unitRaw) return '—'
@@ -13,10 +13,7 @@
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">运输信息</h3>
<div
v-if="showArrangeLinks"
class="pl-plan-arrange-links pl-plan-transport-block"
>
<div v-if="showArrangeLinks" class="pl-plan-arrange-links pl-plan-transport-block">
<div class="pl-arrange-links-body">
<div class="pl-arrange-link-row">
<span class="pl-arrange-link-label">司机链接</span>
@@ -123,6 +120,10 @@
stop-archive-mode="load"
/>
<p v-else class="pl-plan-form-empty-hint">暂无装货产品</p>
<PlPlanArchiveBrandViewBlock
v-if="showArchiveIoFields"
:items="loadArchiveBrandItems"
/>
<PlPlanStopFeedbackPanel
v-if="showStopFeedback && !feedbackMirrorTo"
stop-label="装货"
@@ -136,7 +137,10 @@
</div>
</template>
<template #delivery>
<div v-if="deliveryViews.length > 1" class="pl-plan-delivery-tabs pl-plan-view-delivery-tabs">
<div
v-if="deliveryViews.length > 1"
class="pl-plan-delivery-tabs pl-plan-view-delivery-tabs"
>
<button
v-for="(delivery, index) in deliveryViews"
:key="delivery.id || 'delivery-tab-' + index"
@@ -199,6 +203,10 @@
stop-archive-mode="delivery"
/>
<p v-else class="pl-plan-form-empty-hint">暂无送货产品</p>
<PlPlanArchiveBrandViewBlock
v-if="showArchiveIoFields"
:items="deliveryArchiveBrandItems(delivery, index)"
/>
<PlPlanStopFeedbackPanel
v-if="showStopFeedback && !feedbackMirrorTo"
:stop-label="
@@ -360,7 +368,12 @@
</div>
</el-form-item>
<el-form-item label="现场备注">
<el-input v-model="ioOrderEditForm.siteFeedbackRemark" type="textarea" :rows="3" maxlength="500" />
<el-input
v-model="ioOrderEditForm.siteFeedbackRemark"
type="textarea"
:rows="3"
maxlength="500"
/>
</el-form-item>
<el-form-item label="现场附件">
<AttachmentUploadField v-model="ioOrderEditImagesCsv" />
@@ -392,6 +405,7 @@
} from '@/components/business/brand-qty-chips-field/index.vue'
import PlPlanDriverFeedbackDialog from '@/components/business/pl-plan-driver-feedback-dialog/index.vue'
import PlPlanHazardCertViewTable from '@/components/business/pl-plan-hazard-cert-view-table/index.vue'
import PlPlanArchiveBrandViewBlock from '@/components/business/pl-plan-archive-brand-view-block/index.vue'
import PlPlanStopFeedbackPanel from '@/components/business/pl-plan-stop-feedback-panel/index.vue'
import PlPlanStopProductTable from '@/components/business/pl-plan-stop-product-table/index.vue'
import PlPlanStopsSplitShell from '@/components/business/pl-plan-stops-split-shell/index.vue'
@@ -433,6 +447,7 @@
type PlanLoadForm,
type PlanStopProductFormRow
} from '@/utils/pl-plan-stop-form'
import { buildArchiveBrandViewsBySegment } from '@/utils/pl-plan-archive-brand-view'
import { resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
import { useDictStore } from '@/store/modules/dict'
@@ -579,8 +594,7 @@
{
label: '状态',
value: statusMap[status] || status,
tagType:
status === 'completed' ? 'success' : status === 'voided' ? 'info' : 'warning'
tagType: status === 'completed' ? 'success' : status === 'voided' ? 'info' : 'warning'
},
{ label: '计划号', value: ioOrder.value.planNo },
{ label: '车牌', value: ioOrder.value.vehicleNo },
@@ -608,18 +622,15 @@
it.brandId &&
!options.some((o) => o.brandId === it.brandId)
) {
options = [
{ brandId: it.brandId, brandName: it.brandName || '指定品牌' },
...options
]
options = [{ brandId: it.brandId, brandName: it.brandName || '指定品牌' }, ...options]
}
return options
}
function mapIoItemBrandQtyRows(it: PlIoOrder['items'][number]): BrandQtyRow[] {
const saved = (it.brandQtys || [])
.filter(r => Number(r.qty) > 0)
.map(r => ({
.filter((r) => Number(r.qty) > 0)
.map((r) => ({
brandId: r.brandId,
brandName: r.brandName || '',
qty: Number(r.qty)
@@ -639,7 +650,7 @@
]
}
if (it.brandMode === 'any') {
return (it.brandOptions || []).map(opt => ({
return (it.brandOptions || []).map((opt) => ({
brandId: opt.brandId,
brandName: opt.brandName || '',
qty: 0
@@ -650,16 +661,14 @@
function openIoOrderEditDialog() {
if (!ioOrder.value) return
const planProductById = new Map((products.value || []).map(p => [p.id, p]))
const planProductById = new Map((products.value || []).map((p) => [p.id, p]))
ioOrderEditForm.value = {
...ioOrder.value,
items: (ioOrder.value.items || []).map(it => {
items: (ioOrder.value.items || []).map((it) => {
const brandOptions = resolveIoBrandOptions(it)
const brandQtyRows = mapIoItemBrandQtyRows({ ...it, brandOptions })
const actualQty = it.brandMode
? Number(
brandQtyRows.reduce((sum, r) => sum + Number(r.qty || 0), 0).toFixed(3)
)
? Number(brandQtyRows.reduce((sum, r) => sum + Number(r.qty || 0), 0).toFixed(3))
: it.actualQty
const planProduct = planProductById.get(it.planProductId)
return {
@@ -684,7 +693,7 @@
for (const it of items) {
if (!it.brandMode) continue
const rows = it.brandQtyRows || []
const positive = rows.filter(r => Number(r.qty) > 0)
const positive = rows.filter((r) => Number(r.qty) > 0)
if (!positive.length) {
ElMessage.warning(`「${it.productName || '产品'}」请至少填写一个品牌数量`)
return
@@ -699,18 +708,21 @@
ioOrderSaving.value = true
try {
const images = ioOrderEditImagesCsv.value
? ioOrderEditImagesCsv.value.split(',').map(s => s.trim()).filter(Boolean)
? ioOrderEditImagesCsv.value
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: []
await api.patch(ioOrderEditForm.value.id, {
siteFeedbackRemark: ioOrderEditForm.value.siteFeedbackRemark,
siteFeedbackImages: images,
items: items.map(it => ({
items: items.map((it) => ({
id: it.id,
actualQty: Number(it.actualQty ?? 0),
brandQtys: it.brandMode
? (it.brandQtyRows || [])
.filter(r => Number(r.qty) > 0)
.map(r => ({
.filter((r) => Number(r.qty) > 0)
.map((r) => ({
brandId: r.brandId,
brandName: r.brandName,
qty: Number(r.qty)
@@ -777,9 +789,13 @@
productRows.value = enriched
}
watch([products, contractSnapshot], () => {
void refreshProductRows()
}, { immediate: true, deep: true })
watch(
[products, contractSnapshot],
() => {
void refreshProductRows()
},
{ immediate: true, deep: true }
)
const planProductById = computed(() => {
const map = new Map<string, (typeof products.value)[number]>()
@@ -797,7 +813,9 @@
return map
})
function resolveFeedbackPlanProductId(row: TransportDriverProductFeedback | Record<string, unknown>) {
function resolveFeedbackPlanProductId(
row: TransportDriverProductFeedback | Record<string, unknown>
) {
const raw =
(row as TransportDriverProductFeedback).planProductId ||
(row as { plan_product_id?: string }).plan_product_id ||
@@ -873,7 +891,9 @@
const fb = feedbackById.get(String(pp.id))
if (!fb) continue
usedIds.add(String(pp.id))
rows.push(buildDriverFeedbackDisplayRow(pp, fb, planProductFormById.value.get(String(pp.id))))
rows.push(
buildDriverFeedbackDisplayRow(pp, fb, planProductFormById.value.get(String(pp.id)))
)
}
// 兼容:planProductId 对不上时按序号兜底
@@ -891,7 +911,9 @@
const id = resolveFeedbackPlanProductId(fb)
if (id && usedIds.has(id)) continue
const pp = id ? planProductById.value.get(id) : undefined
rows.push(buildDriverFeedbackDisplayRow(pp, fb, id ? planProductFormById.value.get(id) : undefined))
rows.push(
buildDriverFeedbackDisplayRow(pp, fb, id ? planProductFormById.value.get(id) : undefined)
)
}
return rows
}
@@ -963,8 +985,7 @@
String(formRow?.productNo || '').trim(),
String(pp?.productNo || '').trim()
].filter(Boolean)
const productNo =
candidates.find((no) => no && no !== productId) || candidates[0] || ''
const productNo = candidates.find((no) => no && no !== productId) || candidates[0] || ''
const brandLabel = resolveDisplayBrandLabel({
brandMode: it.brandMode || pp?.brandMode || formRow?.brandMode,
brandName: it.brandName || pp?.brandName || formRow?.brandName
@@ -986,10 +1007,7 @@
remark: (it.remark || pp?.remark || formRow?.remark || '').trim(),
unit: unitCode || it.unit,
unitLabel:
it.unitLabel ||
dictStore.getLabel('crm_product_unit', unitCode) ||
unitCode ||
''
it.unitLabel || dictStore.getLabel('crm_product_unit', unitCode) || unitCode || ''
}
})
})
@@ -1006,7 +1024,7 @@
)
const dibangPlanRows = computed(() =>
dibangRows.value.filter(row => Boolean(row.link.dbPurchasePlanId?.trim()))
dibangRows.value.filter((row) => Boolean(row.link.dbPurchasePlanId?.trim()))
)
const siteInventoryRows = computed(() => siteRows.value)
@@ -1169,11 +1187,15 @@
ElMessage.warning('暂无现场链接')
return
}
const product = products.value.find(item => item.id === row.link.planProductId)
const product = products.value.find((item) => item.id === row.link.planProductId)
const productText = product
? formatPlanProductListText(product, unit => dictStore.getLabel('crm_product_unit', unit) || unit, {
usePlannedQuantity: true
})
? formatPlanProductListText(
product,
(unit) => dictStore.getLabel('crm_product_unit', unit) || unit,
{
usePlannedQuantity: true
}
)
: row.link.productName || '—'
const text = buildSiteSummaryFromInventoryLinks({
planNo: row.link.planNo || plan.value?.planNo,
@@ -1212,12 +1234,14 @@
const deliveryViews = computed(() => {
if (plan.value?.deliveries?.length) {
return mapDeliveriesFromApi(plan.value.deliveries, plan.value.load?.customerName).map((row, index) => ({
id: plan.value?.deliveries?.[index]?.id,
...row,
address: resolveDeliveryViewAddress(plan.value, row, index),
products: resolveStopProductsForView(row.products, productRows.value)
}))
return mapDeliveriesFromApi(plan.value.deliveries, plan.value.load?.customerName).map(
(row, index) => ({
id: plan.value?.deliveries?.[index]?.id,
...row,
address: resolveDeliveryViewAddress(plan.value, row, index),
products: resolveStopProductsForView(row.products, productRows.value)
})
)
}
return mapLegacyPlanToStops(plan.value || ({} as any)).deliveries.map((row, index) => ({
id: undefined as string | undefined,
@@ -1227,6 +1251,23 @@
}))
})
const archiveBrandViewsBySegment = computed(() =>
buildArchiveBrandViewsBySegment({
loadProducts: loadView.value.products || [],
deliveries: deliveryViews.value.map((row, index) => ({
key: `delivery-${row.id || index}`,
products: row.products || []
})),
planProducts: productRows.value
})
)
const loadArchiveBrandItems = computed(() => archiveBrandViewsBySegment.value.get('load') || [])
function deliveryArchiveBrandItems(delivery: { id?: string }, index: number) {
return archiveBrandViewsBySegment.value.get(`delivery-${delivery.id || index}`) || []
}
watch(
() => deliveryViews.value.length,
(len) => {
@@ -1337,7 +1378,6 @@
() => plan.value?.transportStatus === 'arranged' && plan.value?.followStatus !== 'cancelled'
)
const feedbackBlocksBind = computed(() => ({
feedbackEditable: props.feedbackEditable,
showArchiveIoFields: props.showArchiveIoFields,
@@ -1405,10 +1445,7 @@
const qty = (transport.value as Transport & { transportQty?: number })?.transportQty
if (qty != null) return String(qty)
if (!productRows.value.length) return '—'
const sum = productRows.value.reduce(
(total, item) => total + Number(item.actualQty ?? 0),
0
)
const sum = productRows.value.reduce((total, item) => total + Number(item.actualQty ?? 0), 0)
return sum ? String(sum) : '—'
})