提交
This commit is contained in:
1 parent
4611dbc2b2
commit
ebf006eeff
46 files changed
+36802
-20974
No files matched your search
@@ -441,6 +441,8 @@ export interface PlanStopProduct {
|
||||
outboundQty?: number
|
||||
outboundPrice?: number
|
||||
outboundAmount?: number
|
||||
/** 本段归档品牌数量(与归档弹窗填写一致) */
|
||||
brandQtys?: Array<{ brandId: string; brandName?: string; qty: number; unitPrice?: number }>
|
||||
remark?: string
|
||||
}
|
||||
|
||||
@@ -623,6 +625,8 @@ export interface PlanQuery {
|
||||
hasHazardProduct?: number
|
||||
/** incomplete=至少一张必需危化证书未上传 */
|
||||
certUploadStatus?: string
|
||||
/** 0=未确认齐备 1=已确认齐备 */
|
||||
hazardCertReady?: number
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
@@ -727,6 +731,8 @@ export interface PlanArchiveBrandQtyItem {
|
||||
brandId: string
|
||||
brandName?: string
|
||||
qty: number
|
||||
/** 本品牌单价(金额 = qty * unitPrice) */
|
||||
unitPrice?: number
|
||||
}
|
||||
|
||||
export interface TransportDriverProductFeedback {
|
||||
@@ -760,6 +766,7 @@ export interface PlanArchiveStopProductItem {
|
||||
inboundPrice?: number
|
||||
outboundQty?: number
|
||||
outboundPrice?: number
|
||||
brandQtys?: PlanArchiveBrandQtyItem[]
|
||||
}
|
||||
|
||||
export interface PlanArchiveLoadBlock {
|
||||
@@ -826,6 +833,10 @@ export interface PlanContractGapItem {
|
||||
supplierContracts: PlanContractLink[]
|
||||
demanderContracts: PlanContractLink[]
|
||||
products?: PlanProduct[]
|
||||
/** 段次合同完善:装货段 */
|
||||
load?: PlanLoad
|
||||
/** 段次合同完善:送货段列表 */
|
||||
deliveries?: PlanDelivery[]
|
||||
}
|
||||
|
||||
export interface PlanContractGapQuery {
|
||||
@@ -919,7 +930,11 @@ export const planApi = {
|
||||
exportContractGaps: (data: Record<string, any>) =>
|
||||
request.post('/transport/plan/contract-gaps/export', data, { responseType: 'blob' }),
|
||||
updateSideContracts: (id: string, side: 'supplier' | 'demander', contracts: PlanContractLink[]) =>
|
||||
request.put<PlanContractGapItem>(`/transport/plan/${id}/contracts/${side}`, { contracts })
|
||||
request.put<PlanContractGapItem>(`/transport/plan/${id}/contracts/${side}`, { contracts }),
|
||||
updateStopContracts: (
|
||||
id: string,
|
||||
data: { stopType: 'load' | 'delivery'; stopId: string; contracts: PlanContractLink[] }
|
||||
) => request.put<PlanContractGapItem>(`/transport/plan/${id}/stop-contracts`, data)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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({
|
||||
|
||||
+192
-164
@@ -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) : '—'
|
||||
})
|
||||
|
||||
|
||||
@@ -40,7 +40,11 @@ export function mapPlanProductToFormRow(item: PlanProduct, index: number): PlanF
|
||||
.map(row => ({
|
||||
brandId: String(row.brandId || ''),
|
||||
brandName: String(row.brandName || ''),
|
||||
qty: Number(row.qty || 0)
|
||||
qty: Number(row.qty || 0),
|
||||
price:
|
||||
row.unitPrice != null && Number.isFinite(Number(row.unitPrice))
|
||||
? Number(row.unitPrice)
|
||||
: undefined
|
||||
}))
|
||||
.filter(row => Number.isFinite(row.qty) && row.qty > 0)
|
||||
return {
|
||||
|
||||
@@ -28,8 +28,7 @@ function isTruthyHazardFlag(value: unknown) {
|
||||
}
|
||||
|
||||
export function resolveProductHazardFlags(product: HazardFlagProduct) {
|
||||
const isToxic =
|
||||
isTruthyHazardFlag(product.isToxic) || isTruthyHazardFlag(product.is_toxic)
|
||||
const isToxic = isTruthyHazardFlag(product.isToxic) || isTruthyHazardFlag(product.is_toxic)
|
||||
const isExplosive =
|
||||
isTruthyHazardFlag(product.isExplosive) || isTruthyHazardFlag(product.is_explosive)
|
||||
return { isToxic, isExplosive }
|
||||
@@ -70,7 +69,7 @@ export function getProductCertUrl(product: PlanProduct, key: HazardCertSlotKey):
|
||||
export function splitProductCertUrls(raw?: string): string[] {
|
||||
return String(raw || '')
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -86,7 +85,7 @@ export function isProductCertSlotFilled(product: PlanProduct, key: HazardCertSlo
|
||||
export function isProductCertComplete(product: PlanProduct): boolean {
|
||||
const slots = getRequiredCertSlots(product)
|
||||
if (!slots.length) return true
|
||||
return slots.every(slot => isProductCertSlotFilled(product, slot.key))
|
||||
return slots.every((slot) => isProductCertSlotFilled(product, slot.key))
|
||||
}
|
||||
|
||||
/** 产品名称后追加易制毒/易制爆标识:毒、爆,或「毒 爆」 */
|
||||
@@ -101,19 +100,17 @@ export function formatPlProductNameWithHazardMarks(product: HazardFlagProduct) {
|
||||
}
|
||||
|
||||
/** 从 CRM 产品主数据读取易制毒/易制爆标识(覆盖计划产品快照字段) */
|
||||
export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduct & { productId: string }>(
|
||||
products: T[]
|
||||
): Promise<T[]> {
|
||||
export async function enrichPlanProductsHazardFromCrm<
|
||||
T extends HazardFlagProduct & { productId: string }
|
||||
>(products: T[]): Promise<T[]> {
|
||||
if (!products.length) return products
|
||||
|
||||
const lookupIds = [
|
||||
...new Set(products.map(product => product.productId).filter(Boolean))
|
||||
]
|
||||
const lookupIds = [...new Set(products.map((product) => product.productId).filter(Boolean))]
|
||||
if (!lookupIds.length) return products
|
||||
|
||||
const hazardMap = new Map<string, { isToxic: number; isExplosive: number; taxRate?: number }>()
|
||||
await Promise.all(
|
||||
lookupIds.map(async productId => {
|
||||
lookupIds.map(async (productId) => {
|
||||
try {
|
||||
const res = await fetchProductDetail({ id: productId })
|
||||
const data = (res as { data?: Record<string, unknown> }).data ?? res
|
||||
@@ -130,7 +127,7 @@ export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduc
|
||||
})
|
||||
)
|
||||
|
||||
return products.map(product => {
|
||||
return products.map((product) => {
|
||||
const fromCrm = hazardMap.get(product.productId)
|
||||
if (!fromCrm) return product
|
||||
return {
|
||||
@@ -143,11 +140,11 @@ export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduc
|
||||
}
|
||||
|
||||
export function findMissingHazardCerts(products: PlanProduct[]) {
|
||||
return products.filter(p => isHazardProduct(p) && !isProductCertComplete(p))
|
||||
return products.filter((p) => isHazardProduct(p) && !isProductCertComplete(p))
|
||||
}
|
||||
|
||||
export function planHasHazardProducts(products: PlanProduct[] | undefined) {
|
||||
return (products ?? []).some(p => isHazardProduct(p))
|
||||
return (products ?? []).some((p) => isHazardProduct(p))
|
||||
}
|
||||
|
||||
export function isHazardCertReady(plan: { hazardCertReady?: number } | null | undefined) {
|
||||
@@ -179,8 +176,8 @@ export function buildMissingHazardCertReadyMessage(
|
||||
|
||||
function formatMissingCertLabels(product: PlanProduct): string {
|
||||
const missing = getRequiredCertSlots(product)
|
||||
.filter(slot => !isProductCertSlotFilled(product, slot.key))
|
||||
.map(slot => slot.label)
|
||||
.filter((slot) => !isProductCertSlotFilled(product, slot.key))
|
||||
.map((slot) => slot.label)
|
||||
return missing.join('、')
|
||||
}
|
||||
|
||||
@@ -188,15 +185,12 @@ export function buildMissingHazardCertMessage(products: PlanProduct[]) {
|
||||
const missing = findMissingHazardCerts(products)
|
||||
if (!missing.length) return ''
|
||||
const details = missing
|
||||
.map(p => `「${p.productName}」须补充:${formatMissingCertLabels(p)}`)
|
||||
.map((p) => `「${p.productName}」须补充:${formatMissingCertLabels(p)}`)
|
||||
.join(';')
|
||||
return `以下产品证书尚未齐全:${details}`
|
||||
}
|
||||
|
||||
export const HAZARD_CERT_REQUIREMENT_LINES = [
|
||||
'易制毒:购买证、运输证',
|
||||
'易制爆:合法证明'
|
||||
] as const
|
||||
export const HAZARD_CERT_REQUIREMENT_LINES = ['易制毒:购买证、运输证', '易制爆:合法证明'] as const
|
||||
|
||||
export const HAZARD_CERT_READY_CONFIRM_HINT =
|
||||
'计划内产品仅分为易制毒、易制爆或普通产品,同一产品不会兼具两种危化属性。' +
|
||||
@@ -213,3 +207,23 @@ export function buildHazardCertReadyConfirmMessage(): string {
|
||||
'确认后可安排运输;之后仍可在本页补充上传附件。'
|
||||
].join('<br>')
|
||||
}
|
||||
|
||||
/** 批量证书齐备确认弹窗正文 */
|
||||
export function buildBatchHazardCertReadyConfirmMessage(count: number): string {
|
||||
return [
|
||||
`确认将所选 ${count} 条运输计划标记为危化品证书已齐备?`,
|
||||
'',
|
||||
...HAZARD_CERT_REQUIREMENT_LINES,
|
||||
'',
|
||||
'确认后可安排运输;之后仍可在本页补充上传附件。'
|
||||
].join('<br>')
|
||||
}
|
||||
|
||||
/** 批量取消证书齐备确认弹窗正文 */
|
||||
export function buildBatchHazardCertUnmarkConfirmMessage(count: number): string {
|
||||
const prefix =
|
||||
count === 1
|
||||
? '确认取消该计划的证书齐备状态?'
|
||||
: `确认取消所选 ${count} 条运输计划的证书齐备状态?`
|
||||
return `${prefix}取消后将无法安排运输。`
|
||||
}
|
||||
@@ -377,14 +377,21 @@ export function plPlanListTabToFilters(tab: PlPlanListTab): {
|
||||
return { planType: plPlanTypeTabToFilter(tab) }
|
||||
}
|
||||
|
||||
/** 危化品证上传页:证书齐备筛选选项 */
|
||||
export const PL_PLAN_HAZARD_CERT_READY_FILTER_OPTIONS = [
|
||||
{ label: '未齐备', value: '0' },
|
||||
{ label: '已齐备', value: '1' }
|
||||
] as const
|
||||
|
||||
export function plPlanCertTabToFilters(tab: PlPlanCertTab): {
|
||||
planType: string
|
||||
certUploadStatus?: string
|
||||
hazardCertReady?: string
|
||||
} {
|
||||
if (tab === 'cert_incomplete') {
|
||||
return { planType: '', certUploadStatus: 'incomplete' }
|
||||
return { planType: '', certUploadStatus: 'incomplete', hazardCertReady: '0' }
|
||||
}
|
||||
return { planType: plPlanTypeTabToFilter(tab) }
|
||||
return { planType: plPlanTypeTabToFilter(tab), hazardCertReady: '' }
|
||||
}
|
||||
|
||||
export function plPlanTypeFilterToTab(planType?: string): PlPlanTypeTab {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
export type PlanContractDisplayLine = {
|
||||
side: string
|
||||
no: string
|
||||
contractId?: string
|
||||
pending?: boolean
|
||||
}
|
||||
|
||||
@@ -26,8 +27,9 @@ export function planContractSideLines(
|
||||
): PlanContractDisplayLine[] {
|
||||
const lines: PlanContractDisplayLine[] = []
|
||||
for (const item of contracts || []) {
|
||||
const no = item.contractNo || item.contractId
|
||||
if (no) lines.push({ side, no: String(no) })
|
||||
const contractId = item.contractId ? String(item.contractId) : undefined
|
||||
const no = item.contractNo || contractId
|
||||
if (no) lines.push({ side, no: String(no), contractId })
|
||||
}
|
||||
if (lines.length) return lines
|
||||
if (isPlInternalCustomerName(customerName)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Plan, PlanDelivery, PlanLoad, PlanProduct, PlanStopProduct } from '@/api/pl'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import {
|
||||
planContractSideLines,
|
||||
type PlanContractDisplayLine
|
||||
@@ -29,7 +30,9 @@ export function resolvePlanStopsForList(plan: Plan): {
|
||||
load: PlanLoad
|
||||
deliveries: PlanDelivery[]
|
||||
} {
|
||||
if (plan.load && (plan.deliveries?.length ?? 0) > 0) {
|
||||
// 有段次结构时优先用 load/deliveries(含段上 contracts);勿因 deliveries 为空回退 legacy,
|
||||
// 否则会丢掉 stop.contracts,误用空的 supplierContracts/demanderContracts。
|
||||
if (plan.load) {
|
||||
return {
|
||||
load: plan.load,
|
||||
deliveries: plan.deliveries || []
|
||||
@@ -163,6 +166,85 @@ export function getStopCustomerName(row: PlPlanListDisplayRow): string {
|
||||
return (row.stop.customerName || '').trim() || '—'
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档列表:入库/出库列展示方向(按本段客户)。
|
||||
* - 装货:山峡/云栋 → 出库;其他 → 入库
|
||||
* - 送货:山峡/云栋 → 入库;其他 → 出库
|
||||
*/
|
||||
export function resolveArchiveListIoColumn(
|
||||
kind: PlPlanListDisplayRowKind,
|
||||
customerName?: string | null
|
||||
): 'inbound' | 'outbound' {
|
||||
const internal = isPlInternalCustomerName(customerName)
|
||||
if (kind === 'load') return internal ? 'outbound' : 'inbound'
|
||||
return internal ? 'inbound' : 'outbound'
|
||||
}
|
||||
|
||||
/** 段次归档实际数量(装货读 inboundQty,送货读 outboundQty) */
|
||||
export function getStopArchiveActualQty(product: PlanStopProduct, kind: PlPlanListDisplayRowKind): number | null {
|
||||
const raw = kind === 'load' ? product.inboundQty : product.outboundQty
|
||||
if (raw == null || raw === '') return null
|
||||
const num = Number(raw)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
export function getStopArchiveUnitPrice(
|
||||
product: PlanStopProduct,
|
||||
kind: PlPlanListDisplayRowKind
|
||||
): number | null {
|
||||
const raw = kind === 'load' ? product.inboundPrice : product.outboundPrice
|
||||
if (raw == null || raw === '') return null
|
||||
const num = Number(raw)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
export function getStopArchiveAmount(
|
||||
product: PlanStopProduct,
|
||||
kind: PlPlanListDisplayRowKind
|
||||
): number | null {
|
||||
const stored = kind === 'load' ? product.inboundAmount : product.outboundAmount
|
||||
if (stored != null && stored !== '') {
|
||||
const num = Number(stored)
|
||||
if (Number.isFinite(num)) return num
|
||||
}
|
||||
const qty = getStopArchiveActualQty(product, kind)
|
||||
const price = getStopArchiveUnitPrice(product, kind)
|
||||
if (qty == null || price == null) return null
|
||||
const amount = qty * price
|
||||
return Number.isFinite(amount) ? amount : null
|
||||
}
|
||||
|
||||
export function sumStopArchiveActualQty(
|
||||
stopProducts: PlanStopProduct[],
|
||||
kind: PlPlanListDisplayRowKind
|
||||
): number {
|
||||
return stopProducts.reduce((sum, item) => sum + (getStopArchiveActualQty(item, kind) || 0), 0)
|
||||
}
|
||||
|
||||
export function sumStopArchiveIoQty(
|
||||
stopProducts: PlanStopProduct[],
|
||||
kind: PlPlanListDisplayRowKind,
|
||||
customerName: string | null | undefined,
|
||||
column: 'inbound' | 'outbound'
|
||||
): number {
|
||||
if (resolveArchiveListIoColumn(kind, customerName) !== column) return 0
|
||||
return sumStopArchiveActualQty(stopProducts, kind)
|
||||
}
|
||||
|
||||
export function sumStopArchiveUnitPrice(
|
||||
stopProducts: PlanStopProduct[],
|
||||
kind: PlPlanListDisplayRowKind
|
||||
): number {
|
||||
return stopProducts.reduce((sum, item) => sum + (getStopArchiveUnitPrice(item, kind) || 0), 0)
|
||||
}
|
||||
|
||||
export function sumStopArchiveAmount(
|
||||
stopProducts: PlanStopProduct[],
|
||||
kind: PlPlanListDisplayRowKind
|
||||
): number {
|
||||
return stopProducts.reduce((sum, item) => sum + (getStopArchiveAmount(item, kind) || 0), 0)
|
||||
}
|
||||
|
||||
/** 装货行展示供方;送货行留空 */
|
||||
export function getStopSupplierNameDisplay(row: PlPlanListDisplayRow): string {
|
||||
if (row.kind !== 'load') return '—'
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface PlanStopProductFormRow {
|
||||
outboundQty?: number
|
||||
outboundPrice?: number
|
||||
outboundAmount?: number
|
||||
/** 本段归档品牌数量 */
|
||||
brandQtys?: Array<{ brandId: string; brandName?: string; qty: number; unitPrice?: number }>
|
||||
remark?: string
|
||||
isExplosive?: number
|
||||
isToxic?: number
|
||||
@@ -361,6 +363,17 @@ function mapStopProductFromApi(row: PlanStopProduct): PlanStopProductFormRow {
|
||||
outboundQty: row.outboundQty != null ? Number(row.outboundQty) : undefined,
|
||||
outboundPrice: row.outboundPrice != null ? Number(row.outboundPrice) : undefined,
|
||||
outboundAmount: row.outboundAmount != null ? Number(row.outboundAmount) : undefined,
|
||||
brandQtys: (row.brandQtys || [])
|
||||
.filter((item) => Number(item.qty) > 0)
|
||||
.map((item) => ({
|
||||
brandId: String(item.brandId || ''),
|
||||
brandName: item.brandName,
|
||||
qty: Number(item.qty),
|
||||
unitPrice:
|
||||
item.unitPrice != null && Number.isFinite(Number(item.unitPrice))
|
||||
? Number(item.unitPrice)
|
||||
: undefined
|
||||
})),
|
||||
remark: row.remark
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,22 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('stopContract')"
|
||||
label="合同"
|
||||
min-width="180"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanContractCell
|
||||
:lines="getStopContractLines(row)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productName')"
|
||||
label="产品名称"
|
||||
@@ -436,6 +452,9 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
@@ -452,6 +471,9 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="inboundQty"
|
||||
@@ -468,12 +490,53 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="outboundQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productUnitPrice')"
|
||||
label="单价(元)"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="unitPrice"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productAmount')"
|
||||
label="金额(元)"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
archive-list-mode
|
||||
:stop-kind="row.kind"
|
||||
:stop-customer-name="row.stop.customerName"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="amount"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="archiveStatsRowRef"
|
||||
@@ -521,6 +584,8 @@
|
||||
</template>
|
||||
</PlPlanViewDialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
|
||||
<PlPlanArchiveDialog
|
||||
v-model:visible="archiveDialogVisible"
|
||||
:plan-id="archivePlanId"
|
||||
@@ -535,6 +600,7 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import {
|
||||
@@ -550,6 +616,7 @@ import {
|
||||
enrichPlanProductsHazardFromCrm
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPlanArchiveDialog from '@/components/business/pl-plan-archive-dialog/index.vue'
|
||||
import PlPlanDocumentAttachmentCell from '@/components/business/pl-plan-document-attachment-cell/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
@@ -574,11 +641,14 @@ import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
import {
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopContractLines,
|
||||
getStopCustomerName,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime,
|
||||
sumStopActualQty,
|
||||
sumStopArchiveActualQty,
|
||||
sumStopArchiveAmount,
|
||||
sumStopArchiveIoQty,
|
||||
sumStopProductsField,
|
||||
type PlPlanListDisplayRow
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
@@ -587,6 +657,7 @@ import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
|
||||
/** 计划区可合并列(顺序须与表格左侧列一致,不含勾选/序号/计划编号) */
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
@@ -627,13 +698,16 @@ const listColumnDefs = [
|
||||
{ prop: 'stopKind', label: '段次' },
|
||||
{ prop: 'stopCustomer', label: '客户' },
|
||||
{ prop: 'stopAddressTime', label: '地址 / 时间' },
|
||||
{ prop: 'stopContract', label: '合同' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'productPlanQty', label: '计划数量' },
|
||||
{ prop: 'productActualQty', label: '实际数量' },
|
||||
{ prop: 'productInboundQty', label: '入库数量' },
|
||||
{ prop: 'productOutboundQty', label: '出库数量' }
|
||||
{ prop: 'productOutboundQty', label: '出库数量' },
|
||||
{ prop: 'productUnitPrice', label: '单价(元)' },
|
||||
{ prop: 'productAmount', label: '金额(元)' }
|
||||
]
|
||||
|
||||
const {
|
||||
@@ -642,7 +716,7 @@ const {
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-plan-archive-list-columns-v3', listColumnDefs)
|
||||
} = usePersistedListColumns('pl-plan-archive-list-columns-v5', listColumnDefs)
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
|
||||
@@ -701,10 +775,21 @@ function getArchiveStatCellValue(row: PlPlanListDisplayRow, prop: string): unkno
|
||||
if (prop === 'stopAddressTime') {
|
||||
return `${getStopAddress(row)} ${getStopTime(row)}`.trim()
|
||||
}
|
||||
if (prop === 'stopContract') {
|
||||
return getStopContractLines(row)
|
||||
.map((l) => `${l.side}${l.no}`)
|
||||
.join(' ')
|
||||
}
|
||||
if (prop === 'productPlanQty') return sumStopProductsField(stopProducts, 'planQty')
|
||||
if (prop === 'productActualQty') return sumStopActualQty(plan, stopProducts)
|
||||
if (prop === 'productInboundQty') return sumStopProductsField(stopProducts, 'inboundQty')
|
||||
if (prop === 'productOutboundQty') return sumStopProductsField(stopProducts, 'outboundQty')
|
||||
if (prop === 'productActualQty') return sumStopArchiveActualQty(stopProducts, row.kind)
|
||||
if (prop === 'productInboundQty') {
|
||||
return sumStopArchiveIoQty(stopProducts, row.kind, row.stop.customerName, 'inbound')
|
||||
}
|
||||
if (prop === 'productOutboundQty') {
|
||||
return sumStopArchiveIoQty(stopProducts, row.kind, row.stop.customerName, 'outbound')
|
||||
}
|
||||
if (prop === 'productUnitPrice') return ''
|
||||
if (prop === 'productAmount') return sumStopArchiveAmount(stopProducts, row.kind)
|
||||
if (prop === 'productName') {
|
||||
return stopProducts
|
||||
.map((p) => p.productName || '')
|
||||
|
||||
@@ -31,11 +31,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称">
|
||||
<el-input
|
||||
v-model="searchForm.customerName"
|
||||
placeholder="搜索客户名称"
|
||||
clearable
|
||||
/>
|
||||
<el-input v-model="searchForm.customerName" placeholder="搜索客户名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="计划日期">
|
||||
<el-date-picker
|
||||
@@ -48,6 +44,16 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="证书齐备">
|
||||
<el-select v-model="searchForm.hazardCertReady" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="opt in PL_PLAN_HAZARD_CERT_READY_FILTER_OPTIONS"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-start" />
|
||||
@@ -78,25 +84,9 @@
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
<el-button
|
||||
:class="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? 'btn-main'
|
||||
: 'btn-ghost'
|
||||
"
|
||||
:type="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
? 'primary'
|
||||
: undefined
|
||||
"
|
||||
:disabled="
|
||||
!selectedPlan ||
|
||||
!canManageCertReady(selectedPlan) ||
|
||||
isHazardCertReady(selectedPlan)
|
||||
"
|
||||
:class="canToolbarMarkCertReady ? 'btn-main' : 'btn-ghost'"
|
||||
:type="canToolbarMarkCertReady ? 'primary' : undefined"
|
||||
:disabled="!canToolbarMarkCertReady"
|
||||
:loading="certReadyLoading"
|
||||
@click="handleToolbarMarkCertReady"
|
||||
>
|
||||
@@ -104,16 +94,12 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
:class="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)
|
||||
? 'btn-ghost'
|
||||
: 'btn-main'
|
||||
"
|
||||
:type="
|
||||
selectedPlan &&
|
||||
canManageCertReady(selectedPlan) &&
|
||||
!isHazardCertReady(selectedPlan)
|
||||
selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)
|
||||
? undefined
|
||||
: 'primary'
|
||||
"
|
||||
@@ -124,11 +110,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="
|
||||
!selectedPlan ||
|
||||
!canManageCertReady(selectedPlan) ||
|
||||
!isHazardCertReady(selectedPlan)
|
||||
"
|
||||
:disabled="!canToolbarUnmarkCertReady"
|
||||
:loading="certReadyLoading"
|
||||
@click="handleToolbarUnmarkCertReady"
|
||||
>
|
||||
@@ -335,6 +317,22 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('stopContract')"
|
||||
label="合同"
|
||||
min-width="180"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanContractCell
|
||||
:lines="getStopContractLines(row)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productName')"
|
||||
label="产品名称"
|
||||
@@ -411,6 +409,7 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
@@ -503,6 +502,8 @@
|
||||
</template>
|
||||
</PlPlanViewDialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
|
||||
<PlPlanCertUploadDialog
|
||||
v-model:visible="certDialogVisible"
|
||||
:plan-id="certPlanId"
|
||||
@@ -512,180 +513,134 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
Upload
|
||||
} from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import PlPlanCertUploadDialog from '@/components/business/pl-plan-cert-upload-dialog/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import {
|
||||
buildHazardCertReadyConfirmMessage,
|
||||
isHazardCertReady,
|
||||
planHasHazardProducts
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import PlPlanTypeTabs from '@/components/business/pl-plan-type-tabs/index.vue'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { useListDateShortcut } from '@/composables/useListDateShortcut'
|
||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||
import {
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay,
|
||||
PL_PLAN_CERT_TABS,
|
||||
PL_PLAN_FOLLOW_STATUS_OPTIONS,
|
||||
plPlanCertTabToFilters,
|
||||
type PlPlanCertTab
|
||||
} from '@/utils/pl-display'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
import {
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopCustomerName,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { CircleCheck, CircleClose, RefreshLeft, Search, Upload } from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import PlPlanCertUploadDialog from '@/components/business/pl-plan-cert-upload-dialog/index.vue'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import {
|
||||
buildBatchHazardCertReadyConfirmMessage,
|
||||
buildBatchHazardCertUnmarkConfirmMessage,
|
||||
buildHazardCertReadyConfirmMessage,
|
||||
isHazardCertReady,
|
||||
planHasHazardProducts
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import PlPlanTypeTabs from '@/components/business/pl-plan-type-tabs/index.vue'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { useListDateShortcut } from '@/composables/useListDateShortcut'
|
||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||
import {
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay,
|
||||
PL_PLAN_CERT_TABS,
|
||||
PL_PLAN_FOLLOW_STATUS_OPTIONS,
|
||||
PL_PLAN_HAZARD_CERT_READY_FILTER_OPTIONS,
|
||||
plPlanCertTabToFilters,
|
||||
type PlPlanCertTab
|
||||
} from '@/utils/pl-display'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
import {
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopContractLines,
|
||||
getStopCustomerName,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
const menuStore = useMenuStore()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
'planDate',
|
||||
'planType',
|
||||
'followStatus',
|
||||
'hazardCertReady',
|
||||
'planAttachments',
|
||||
'licensePlate',
|
||||
'personnelSummary',
|
||||
'followUserName'
|
||||
] as const
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
'planDate',
|
||||
'planType',
|
||||
'followStatus',
|
||||
'hazardCertReady',
|
||||
'planAttachments',
|
||||
'licensePlate',
|
||||
'personnelSummary',
|
||||
'followUserName'
|
||||
] as const
|
||||
|
||||
const loading = ref(false)
|
||||
const { mediaActive } = useListMediaActive()
|
||||
const certReadyLoading = ref(false)
|
||||
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
const certDialogVisible = ref(false)
|
||||
const certPlanId = ref('')
|
||||
const planList = ref<Plan[]>([])
|
||||
const loading = ref(false)
|
||||
const { mediaActive } = useListMediaActive()
|
||||
const certReadyLoading = ref(false)
|
||||
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
const certDialogVisible = ref(false)
|
||||
const certPlanId = ref('')
|
||||
const planList = ref<Plan[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'planDate', label: '计划日期' },
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'followStatus', label: '跟进状态' },
|
||||
{ prop: 'hazardCertReady', label: '证书齐备' },
|
||||
{ prop: 'planAttachments', label: '计划附件' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'personnelSummary', label: '人员' },
|
||||
{ prop: 'followUserName', label: '跟进人', defaultVisible: false },
|
||||
{ prop: 'stopKind', label: '段次' },
|
||||
{ prop: 'stopCustomer', label: '客户' },
|
||||
{ prop: 'stopAddressTime', label: '地址 / 时间' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'productPlanQty', label: '计划数量' },
|
||||
{ prop: 'productActualQty', label: '实际数量' },
|
||||
{ prop: 'productInboundQty', label: '入库数量' },
|
||||
{ prop: 'productOutboundQty', label: '出库数量' }
|
||||
]
|
||||
const listColumnDefs = [
|
||||
{ prop: 'planDate', label: '计划日期' },
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'followStatus', label: '跟进状态' },
|
||||
{ prop: 'hazardCertReady', label: '证书齐备' },
|
||||
{ prop: 'planAttachments', label: '计划附件' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'personnelSummary', label: '人员' },
|
||||
{ prop: 'followUserName', label: '跟进人', defaultVisible: false },
|
||||
{ prop: 'stopKind', label: '段次' },
|
||||
{ prop: 'stopCustomer', label: '客户' },
|
||||
{ prop: 'stopAddressTime', label: '地址 / 时间' },
|
||||
{ prop: 'stopContract', label: '合同' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'productPlanQty', label: '计划数量' },
|
||||
{ prop: 'productActualQty', label: '实际数量' },
|
||||
{ prop: 'productInboundQty', label: '入库数量' },
|
||||
{ prop: 'productOutboundQty', label: '出库数量' }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-plan-cert-list-columns-v3', listColumnDefs)
|
||||
const { visibleColumns, checkAll, isIndeterminate, isColumnVisible, resetToDefault } =
|
||||
usePersistedListColumns('pl-plan-cert-list-columns-v4', listColumnDefs)
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const visiblePlanMergeProps = computed(() =>
|
||||
PLAN_MERGE_COLUMN_PROPS.filter((prop) => isColumnVisible(prop))
|
||||
)
|
||||
const visiblePlanMergeProps = computed(() =>
|
||||
PLAN_MERGE_COLUMN_PROPS.filter((prop) => isColumnVisible(prop))
|
||||
)
|
||||
|
||||
const {
|
||||
displayRows,
|
||||
planListSpanMethod,
|
||||
getDisplayRowIndex,
|
||||
isPlanFirstRowSelectable,
|
||||
handleDisplaySelectionChange,
|
||||
handleDisplayRowDblClick
|
||||
} = usePlPlanMergeListTable({
|
||||
planList,
|
||||
visiblePlanMergeProps,
|
||||
selectedRows,
|
||||
pagination,
|
||||
onView: (plan) => handleView(plan)
|
||||
})
|
||||
function getPlanCertExportFilterParams() {
|
||||
const keyword = quickSearch.value.trim()
|
||||
return {
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
followStatus: searchForm.followStatus || undefined,
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
planDateFrom: dateRange.value?.[0] || undefined,
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
hasHazardProduct: 1,
|
||||
certUploadStatus: searchForm.certUploadStatus || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const planCertExportColumns = computed(() => {
|
||||
const skip = new Set(['stopKind', 'stopCustomer', 'stopAddressTime'])
|
||||
const cols = visibleColumns.value.filter((col) => !skip.has(col))
|
||||
if (!cols.includes('planNo')) cols.unshift('planNo')
|
||||
return cols
|
||||
})
|
||||
|
||||
const viewingPlan = ref<Plan | null>(null)
|
||||
const selectedPlan = computed(() => (selectedRows.value.length === 1 ? selectedRows.value[0] : null))
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const activePlanTypeTab = ref<PlPlanCertTab>('all')
|
||||
|
||||
const searchForm = reactive({
|
||||
planNo: '',
|
||||
planType: '',
|
||||
followStatus: '',
|
||||
customerName: '',
|
||||
certUploadStatus: '' as string
|
||||
})
|
||||
|
||||
function getPlanTypeLabel(type: string) {
|
||||
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const {
|
||||
displayRows,
|
||||
planListSpanMethod,
|
||||
getDisplayRowIndex,
|
||||
isPlanFirstRowSelectable,
|
||||
handleDisplaySelectionChange,
|
||||
handleDisplayRowDblClick
|
||||
} = usePlPlanMergeListTable({
|
||||
planList,
|
||||
visiblePlanMergeProps,
|
||||
selectedRows,
|
||||
pagination,
|
||||
onView: (plan) => handleView(plan)
|
||||
})
|
||||
function getPlanCertExportFilterParams() {
|
||||
const keyword = quickSearch.value.trim()
|
||||
const res = await planApi.list({
|
||||
return {
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
@@ -695,194 +650,352 @@ async function fetchData() {
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
hasHazardProduct: 1,
|
||||
certUploadStatus: searchForm.certUploadStatus || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
planList.value = data.list || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
hazardCertReady:
|
||||
searchForm.hazardCertReady === '' ? undefined : Number(searchForm.hazardCertReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handlePlanTypeTabChange(tab: PlPlanCertTab | string) {
|
||||
const filters = plPlanCertTabToFilters(tab as PlPlanCertTab)
|
||||
searchForm.planType = filters.planType
|
||||
searchForm.certUploadStatus = filters.certUploadStatus || ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
activePlanTypeTab.value = 'all'
|
||||
searchForm.planNo = ''
|
||||
searchForm.planType = ''
|
||||
searchForm.followStatus = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.certUploadStatus = ''
|
||||
quickSearch.value = ''
|
||||
resetDateShortcut()
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleQuickSearch() {
|
||||
searchForm.planNo = ''
|
||||
searchForm.customerName = ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearch.value = ''
|
||||
handleQuickSearch()
|
||||
}
|
||||
|
||||
const { activeDateShortcut, handleDateShortcut, resetDateShortcut } = useListDateShortcut(
|
||||
dateRange,
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handlePageChange() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function canUploadCert(row: Plan) {
|
||||
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
|
||||
}
|
||||
|
||||
function canManageCertReady(row: Plan) {
|
||||
return canUploadCert(row) && planHasHazardProducts(row.products)
|
||||
}
|
||||
|
||||
function getHazardCertReadyLabel(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return '无需'
|
||||
return isHazardCertReady(row) ? '已齐备' : '待确认'
|
||||
}
|
||||
|
||||
function getHazardCertReadyTagType(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return 'info'
|
||||
return isHazardCertReady(row) ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
async function refreshSidebarBadges() {
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
}
|
||||
|
||||
async function handleMarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm(buildHazardCertReadyConfirmMessage(), '证书齐备确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认齐备',
|
||||
cancelButtonText: '取消',
|
||||
dangerouslyUseHTMLString: true
|
||||
const planCertExportColumns = computed(() => {
|
||||
const skip = new Set(['stopKind', 'stopCustomer', 'stopAddressTime', 'stopContract'])
|
||||
const cols = visibleColumns.value.filter((col) => !skip.has(col))
|
||||
if (!cols.includes('planNo')) cols.unshift('planNo')
|
||||
return cols
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, true)
|
||||
ElMessage.success('已标记证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnmarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm('确认取消该计划的证书齐备状态?取消后将无法安排运输。', '取消齐备', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认取消',
|
||||
cancelButtonText: '返回'
|
||||
const viewingPlan = ref<Plan | null>(null)
|
||||
const selectedPlan = computed(() =>
|
||||
selectedRows.value.length === 1 ? selectedRows.value[0] : null
|
||||
)
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const activePlanTypeTab = ref<PlPlanCertTab>('all')
|
||||
|
||||
const searchForm = reactive({
|
||||
planNo: '',
|
||||
planType: '',
|
||||
followStatus: '',
|
||||
customerName: '',
|
||||
certUploadStatus: '' as string,
|
||||
hazardCertReady: '' as string
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, false)
|
||||
ElMessage.success('已取消证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
|
||||
function getPlanTypeLabel(type: string) {
|
||||
const map: Record<string, string> = { direct: '直销单', inbound: '入库单', outbound: '出库单' }
|
||||
return map[type] || type
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarMarkCertReady() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleMarkCertReady(selectedRows.value[0])
|
||||
}
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const keyword = quickSearch.value.trim()
|
||||
const res = await planApi.list({
|
||||
q: keyword || undefined,
|
||||
planNo: keyword ? undefined : searchForm.planNo || undefined,
|
||||
planType: searchForm.planType || undefined,
|
||||
followStatus: searchForm.followStatus || undefined,
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
planDateFrom: dateRange.value?.[0] || undefined,
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
hasHazardProduct: 1,
|
||||
certUploadStatus: searchForm.certUploadStatus || undefined,
|
||||
hazardCertReady:
|
||||
searchForm.hazardCertReady === '' ? undefined : Number(searchForm.hazardCertReady),
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
planList.value = data.list || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarUnmarkCertReady() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleUnmarkCertReady(selectedRows.value[0])
|
||||
}
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function syncViewingPlanFromList() {
|
||||
syncViewRowFromFlatList(viewingPlan, planList.value)
|
||||
}
|
||||
function handlePlanTypeTabChange(tab: PlPlanCertTab | string) {
|
||||
const filters = plPlanCertTabToFilters(tab as PlPlanCertTab)
|
||||
searchForm.planType = filters.planType
|
||||
searchForm.certUploadStatus = filters.certUploadStatus || ''
|
||||
searchForm.hazardCertReady = filters.hazardCertReady ?? ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
async function refreshViewIfOpen() {
|
||||
if (!viewVisible.value || !viewPlanId.value) return
|
||||
syncViewingPlanFromList()
|
||||
await viewDialogRef.value?.reload()
|
||||
}
|
||||
|
||||
async function handleCertUploadSuccess() {
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
}
|
||||
|
||||
function handleView(row: Plan) {
|
||||
viewingPlan.value = row
|
||||
viewPlanId.value = row.id
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleUploadCert(row: Plan) {
|
||||
certPlanId.value = row.id
|
||||
certDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleToolbarUploadCert() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleUploadCert(selectedRows.value[0])
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
planNo: searchForm.planNo,
|
||||
planType: searchForm.planType,
|
||||
followStatus: searchForm.followStatus,
|
||||
customerName: searchForm.customerName,
|
||||
certUploadStatus: searchForm.certUploadStatus,
|
||||
dateRange: dateRange.value
|
||||
}),
|
||||
() => {
|
||||
function handleReset() {
|
||||
activePlanTypeTab.value = 'all'
|
||||
searchForm.planNo = ''
|
||||
searchForm.planType = ''
|
||||
searchForm.followStatus = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.certUploadStatus = ''
|
||||
searchForm.hazardCertReady = ''
|
||||
quickSearch.value = ''
|
||||
handleSearch()
|
||||
resetDateShortcut()
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
function handleQuickSearch() {
|
||||
searchForm.planNo = ''
|
||||
searchForm.customerName = ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearch.value = ''
|
||||
handleQuickSearch()
|
||||
}
|
||||
|
||||
const { activeDateShortcut, handleDateShortcut, resetDateShortcut } = useListDateShortcut(
|
||||
dateRange,
|
||||
handleSearch
|
||||
)
|
||||
|
||||
function handlePageChange() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function canUploadCert(row: Plan) {
|
||||
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
|
||||
}
|
||||
|
||||
function canManageCertReady(row: Plan) {
|
||||
return canUploadCert(row) && planHasHazardProducts(row.products)
|
||||
}
|
||||
|
||||
const certReadyEligibleRows = computed(() =>
|
||||
selectedRows.value.filter((row) => canManageCertReady(row) && !isHazardCertReady(row))
|
||||
)
|
||||
const canToolbarMarkCertReady = computed(() => certReadyEligibleRows.value.length > 0)
|
||||
const certUnmarkEligibleRows = computed(() =>
|
||||
selectedRows.value.filter((row) => canManageCertReady(row) && isHazardCertReady(row))
|
||||
)
|
||||
const canToolbarUnmarkCertReady = computed(() => certUnmarkEligibleRows.value.length > 0)
|
||||
|
||||
function getHazardCertReadyLabel(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return '无需'
|
||||
return isHazardCertReady(row) ? '已齐备' : '待确认'
|
||||
}
|
||||
|
||||
function getHazardCertReadyTagType(row: Plan) {
|
||||
if (!planHasHazardProducts(row.products)) return 'info'
|
||||
return isHazardCertReady(row) ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
async function refreshSidebarBadges() {
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
}
|
||||
|
||||
async function handleMarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm(buildHazardCertReadyConfirmMessage(), '证书齐备确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认齐备',
|
||||
cancelButtonText: '取消',
|
||||
dangerouslyUseHTMLString: true
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, true)
|
||||
ElMessage.success('已标记证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnmarkCertReady(row: Plan) {
|
||||
await ElMessageBox.confirm('确认取消该计划的证书齐备状态?取消后将无法安排运输。', '取消齐备', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认取消',
|
||||
cancelButtonText: '返回'
|
||||
})
|
||||
certReadyLoading.value = true
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, false)
|
||||
ElMessage.success('已取消证书齐备')
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarMarkCertReady() {
|
||||
void handleBatchMarkCertReady()
|
||||
}
|
||||
|
||||
async function handleBatchMarkCertReady() {
|
||||
if (!selectedRows.value.length) {
|
||||
ElMessage.warning('请先勾选运输计划')
|
||||
return
|
||||
}
|
||||
const targets = certReadyEligibleRows.value
|
||||
const skipped = selectedRows.value.length - targets.length
|
||||
if (!targets.length) {
|
||||
ElMessage.warning('所选计划均不可标记证书齐备(已齐备、已归档/作废或无危化品)')
|
||||
return
|
||||
}
|
||||
|
||||
const confirmMessage =
|
||||
targets.length === 1
|
||||
? buildHazardCertReadyConfirmMessage()
|
||||
: buildBatchHazardCertReadyConfirmMessage(targets.length)
|
||||
|
||||
await ElMessageBox.confirm(confirmMessage, '证书齐备确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认齐备',
|
||||
cancelButtonText: '取消',
|
||||
dangerouslyUseHTMLString: true
|
||||
})
|
||||
|
||||
certReadyLoading.value = true
|
||||
let success = 0
|
||||
let failed = 0
|
||||
try {
|
||||
for (const row of targets) {
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, true)
|
||||
success += 1
|
||||
} catch {
|
||||
failed += 1
|
||||
}
|
||||
}
|
||||
if (failed === 0) {
|
||||
const skippedHint = skipped > 0 ? `,已跳过 ${skipped} 条不可操作项` : ''
|
||||
ElMessage.success(`已标记 ${success} 条计划证书齐备${skippedHint}`)
|
||||
} else {
|
||||
ElMessage.warning(`成功 ${success} 条,失败 ${failed} 条`)
|
||||
}
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarUnmarkCertReady() {
|
||||
void handleBatchUnmarkCertReady()
|
||||
}
|
||||
|
||||
async function handleBatchUnmarkCertReady() {
|
||||
if (!selectedRows.value.length) {
|
||||
ElMessage.warning('请先勾选运输计划')
|
||||
return
|
||||
}
|
||||
const targets = certUnmarkEligibleRows.value
|
||||
const skipped = selectedRows.value.length - targets.length
|
||||
if (!targets.length) {
|
||||
ElMessage.warning('所选计划均不可取消证书齐备(未齐备、已归档/作废或无危化品)')
|
||||
return
|
||||
}
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
buildBatchHazardCertUnmarkConfirmMessage(targets.length),
|
||||
'取消齐备',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认取消',
|
||||
cancelButtonText: '返回'
|
||||
}
|
||||
)
|
||||
|
||||
certReadyLoading.value = true
|
||||
let success = 0
|
||||
let failed = 0
|
||||
try {
|
||||
for (const row of targets) {
|
||||
try {
|
||||
await planApi.setHazardCertReady(row.id, false)
|
||||
success += 1
|
||||
} catch {
|
||||
failed += 1
|
||||
}
|
||||
}
|
||||
if (failed === 0) {
|
||||
const skippedHint = skipped > 0 ? `,已跳过 ${skipped} 条不可操作项` : ''
|
||||
ElMessage.success(`已取消 ${success} 条计划证书齐备${skippedHint}`)
|
||||
} else {
|
||||
ElMessage.warning(`成功 ${success} 条,失败 ${failed} 条`)
|
||||
}
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
} finally {
|
||||
certReadyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function syncViewingPlanFromList() {
|
||||
syncViewRowFromFlatList(viewingPlan, planList.value)
|
||||
}
|
||||
|
||||
async function refreshViewIfOpen() {
|
||||
if (!viewVisible.value || !viewPlanId.value) return
|
||||
syncViewingPlanFromList()
|
||||
await viewDialogRef.value?.reload()
|
||||
}
|
||||
|
||||
async function handleCertUploadSuccess() {
|
||||
await fetchData()
|
||||
await refreshViewIfOpen()
|
||||
await refreshSidebarBadges()
|
||||
}
|
||||
|
||||
function handleView(row: Plan) {
|
||||
viewingPlan.value = row
|
||||
viewPlanId.value = row.id
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleUploadCert(row: Plan) {
|
||||
certPlanId.value = row.id
|
||||
certDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleToolbarUploadCert() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
handleUploadCert(selectedRows.value[0])
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({
|
||||
planNo: searchForm.planNo,
|
||||
planType: searchForm.planType,
|
||||
followStatus: searchForm.followStatus,
|
||||
customerName: searchForm.customerName,
|
||||
certUploadStatus: searchForm.certUploadStatus,
|
||||
hazardCertReady: searchForm.hazardCertReady,
|
||||
dateRange: dateRange.value
|
||||
}),
|
||||
() => {
|
||||
quickSearch.value = ''
|
||||
handleSearch()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@styles/business/pl-plan-merge-list-table.scss';
|
||||
@use '@styles/business/pl-plan-merge-list-table.scss';
|
||||
|
||||
.pl-plan-list-toolbar-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pl-plan-list-toolbar-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<!-- 运输计划合同完善:仅缺合同计划,行内展开分侧保存 -->
|
||||
<!-- 运输计划合同完善:仅缺合同计划,按段次客户弹窗补充合同 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
@@ -17,15 +17,15 @@
|
||||
<el-form-item label="缺口类型">
|
||||
<el-select v-model="searchForm.gapType" placeholder="全部缺口" clearable>
|
||||
<el-option label="全部缺口" value="" />
|
||||
<el-option label="仅缺供方合同" value="supplier" />
|
||||
<el-option label="仅缺需方合同" value="demander" />
|
||||
<el-option label="两侧均缺" value="both" />
|
||||
<el-option label="装货段缺合同" value="supplier" />
|
||||
<el-option label="送货段缺合同" value="demander" />
|
||||
<el-option label="装货与送货均缺" value="both" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="供方 / 需方">
|
||||
<el-form-item label="客户名称">
|
||||
<el-input
|
||||
v-model="searchForm.customerKeyword"
|
||||
placeholder="客户名称"
|
||||
placeholder="段次客户名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
@@ -66,7 +66,7 @@
|
||||
/>
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearch"
|
||||
placeholder="计划编号/供方/需方"
|
||||
placeholder="计划编号/客户名称"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
@@ -87,107 +87,14 @@
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="list-table pl-plan-merge-list-table"
|
||||
:data="displayRows"
|
||||
v-loading="loading"
|
||||
row-key="rowKey"
|
||||
:span-method="planListSpanMethod"
|
||||
:row-class-name="getPlPlanListRowClassName"
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
@expand-change="handleExpandChange"
|
||||
@row-dblclick="handleDisplayRowDblClick"
|
||||
>
|
||||
<el-table-column type="expand" class-name="pl-contract-expand-col" width="1">
|
||||
<template #default="{ row }">
|
||||
<div class="pl-contract-expand" v-if="expandDraft[row.plan.id]">
|
||||
<div class="pl-contract-expand__grid">
|
||||
<div
|
||||
class="pl-contract-side"
|
||||
:class="sideBoxClass(asGapItem(row), 'supplier')"
|
||||
>
|
||||
<div class="pl-contract-side__head">
|
||||
<h4>供方合同</h4>
|
||||
<el-tag
|
||||
:type="sideTagType(asGapItem(row), 'supplier')"
|
||||
size="small"
|
||||
:effect="asGapItem(row).supplierContractExempt ? 'plain' : 'dark'"
|
||||
>
|
||||
{{ sideTagLabel(asGapItem(row), 'supplier') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="pl-contract-side__customer">
|
||||
{{ asGapItem(row).supplierName || '—' }}
|
||||
</p>
|
||||
<template v-if="asGapItem(row).supplierContractExempt">
|
||||
<p class="pl-contract-side__exempt">内置公司,合同可免</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PlPlanContractMultiSelect
|
||||
v-model="expandDraft[row.plan.id].supplierContracts"
|
||||
:customer-id="asGapItem(row).supplierId"
|
||||
:disabled="!asGapItem(row).supplierId"
|
||||
:required="false"
|
||||
/>
|
||||
<div class="pl-contract-side__actions">
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="savingKey === `${row.plan.id}:supplier`"
|
||||
@click="handleSaveSide(asGapItem(row), 'supplier')"
|
||||
>
|
||||
<el-icon><Check /></el-icon>保存供方合同
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pl-contract-side"
|
||||
:class="sideBoxClass(asGapItem(row), 'demander')"
|
||||
>
|
||||
<div class="pl-contract-side__head">
|
||||
<h4>需方合同</h4>
|
||||
<el-tag
|
||||
:type="sideTagType(asGapItem(row), 'demander')"
|
||||
size="small"
|
||||
:effect="asGapItem(row).demanderContractExempt ? 'plain' : 'dark'"
|
||||
>
|
||||
{{ sideTagLabel(asGapItem(row), 'demander') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="pl-contract-side__customer">
|
||||
{{ asGapItem(row).customerName || '—' }}
|
||||
</p>
|
||||
<template v-if="asGapItem(row).demanderContractExempt">
|
||||
<p class="pl-contract-side__exempt">内置公司,合同可免</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PlPlanContractMultiSelect
|
||||
v-model="expandDraft[row.plan.id].demanderContracts"
|
||||
:customer-id="asGapItem(row).customerId"
|
||||
:disabled="!asGapItem(row).customerId"
|
||||
:required="false"
|
||||
/>
|
||||
<div class="pl-contract-side__actions">
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="savingKey === `${row.plan.id}:demander`"
|
||||
@click="handleSaveSide(asGapItem(row), 'demander')"
|
||||
>
|
||||
<el-icon><Check /></el-icon>保存需方合同
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -278,28 +185,19 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="pl-gap-contract-cell">
|
||||
<div class="pl-plan-contract-cell">
|
||||
<div
|
||||
v-for="(line, index) in getContractGapStopLines(row)"
|
||||
:key="`${line.side}-${line.no}-${index}`"
|
||||
class="pl-plan-contract-cell__row"
|
||||
>
|
||||
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
|
||||
<span
|
||||
class="pl-plan-contract-cell__no"
|
||||
:class="{ 'is-pending': line.pending }"
|
||||
>{{ line.no }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<PlPlanContractCell
|
||||
:lines="getContractGapStopLines(row)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
<el-button
|
||||
v-if="showGapSupplementAction(row)"
|
||||
link
|
||||
type="warning"
|
||||
class="pl-gap-contract-cell__action"
|
||||
@click.stop="toggleExpand(asGapItem(row))"
|
||||
@click.stop="openSupplementDialog(row)"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
{{ isExpanded(asGapItem(row).id) ? '收起' : '补充' }}
|
||||
补充
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -382,6 +280,7 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="asGapItem(row).products"
|
||||
field="actualQty"
|
||||
@@ -441,22 +340,108 @@
|
||||
:plan-id="viewPlanId"
|
||||
show-inbound-outbound-qty
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="supplementVisible"
|
||||
class="dialog-form-layout pl-contract-supplement-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
width="960px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:before-close="handleSupplementBeforeClose"
|
||||
>
|
||||
<template #header>
|
||||
<span class="pl-contract-supplement-dialog__title">
|
||||
补充合同 · {{ supplementStopKindLabel }} · {{ supplementPlanNo }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div v-if="supplementRow" class="pl-contract-supplement-body">
|
||||
<div class="pl-contract-supplement-summary">
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">计划日期</div>
|
||||
<div class="v">{{ asGapItem(supplementRow).planDate || '—' }}</div>
|
||||
</div>
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">跟进状态</div>
|
||||
<div class="v">
|
||||
<el-tag
|
||||
:type="getPlPlanFollowStatusTagType(asGapItem(supplementRow).followStatus)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ getPlPlanFollowStatusLabel(asGapItem(supplementRow).followStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">本段客户</div>
|
||||
<div class="v">{{ getStopCustomerName(supplementRow) }}</div>
|
||||
</div>
|
||||
<div class="pl-contract-supplement-kv">
|
||||
<div class="k">地址 / 时间</div>
|
||||
<div class="v">
|
||||
{{ getStopAddress(supplementRow) }}
|
||||
<template v-if="getStopTime(supplementRow)">
|
||||
· {{ getStopTime(supplementRow) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-contract-side" :class="supplementSideBoxClass">
|
||||
<div class="pl-contract-side__head">
|
||||
<h4>{{ supplementStopKindLabel }}合同</h4>
|
||||
<el-tag :type="supplementSideTagType" size="small" effect="dark">
|
||||
{{ supplementSideTagLabel }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="pl-contract-side__customer">{{ getStopCustomerName(supplementRow) }}</p>
|
||||
<PlPlanContractMultiSelect
|
||||
v-model="supplementDraft.contracts"
|
||||
:customer-id="supplementRow.stop.customerId"
|
||||
:disabled="!supplementRow.stop.customerId"
|
||||
:required="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="requestCloseSupplement">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="!supplementDraft.stopId || !supplementRow?.stop.customerId"
|
||||
:loading="supplementSaving"
|
||||
@click="handleSaveStopContracts"
|
||||
>
|
||||
<el-icon><Check /></el-icon>保存本段合同
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, EditPen, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { Check, Close, EditPen, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
planApi,
|
||||
type PlanContractGapItem,
|
||||
type PlanContractLink
|
||||
} from '@/api/pl'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { useListDateShortcut } from '@/composables/useListDateShortcut'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import {
|
||||
@@ -481,24 +466,29 @@ import { useMenuStore } from '@/store/modules/menu'
|
||||
const PLAN_MERGE_COLUMN_PROPS = ['planDate', 'planType', 'followStatus'] as const
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const tableData = ref<PlanContractGapItem[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const expandedRowKeys = ref<string[]>([])
|
||||
const expandDraft = reactive<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
supplierContracts: PlanContractLink[]
|
||||
demanderContracts: PlanContractLink[]
|
||||
baselineSupplier: string
|
||||
baselineDemander: string
|
||||
}
|
||||
>
|
||||
>({})
|
||||
const savingKey = ref('')
|
||||
const supplementVisible = ref(false)
|
||||
const supplementSaving = ref(false)
|
||||
const supplementRow = ref<PlPlanListDisplayRow | null>(null)
|
||||
const supplementDraft = reactive<{
|
||||
contracts: PlanContractLink[]
|
||||
baseline: string
|
||||
stopType: 'load' | 'delivery'
|
||||
stopId: string
|
||||
planId: string
|
||||
rowKey: string
|
||||
}>({
|
||||
contracts: [],
|
||||
baseline: '',
|
||||
stopType: 'load',
|
||||
stopId: '',
|
||||
planId: '',
|
||||
rowKey: ''
|
||||
})
|
||||
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
@@ -523,8 +513,7 @@ const {
|
||||
displayRows,
|
||||
planListSpanMethod,
|
||||
getDisplayRowIndex,
|
||||
handleDisplayRowDblClick,
|
||||
findPlanFirstDisplayRow
|
||||
handleDisplayRowDblClick
|
||||
} = usePlPlanMergeListTable({
|
||||
planList: planListForDisplay,
|
||||
visiblePlanMergeProps,
|
||||
@@ -533,11 +522,28 @@ const {
|
||||
onView: (plan) => handleView(plan as unknown as PlanContractGapItem)
|
||||
})
|
||||
|
||||
const supplementStopKindLabel = computed(() =>
|
||||
supplementRow.value ? getStopKindLabel(supplementRow.value) : ''
|
||||
)
|
||||
const supplementPlanNo = computed(() =>
|
||||
supplementRow.value ? asGapItem(supplementRow.value).planNo || '—' : '—'
|
||||
)
|
||||
const supplementSideBoxClass = computed(() => {
|
||||
if (!supplementDraft.contracts.length) return 'is-need'
|
||||
return 'is-ok'
|
||||
})
|
||||
const supplementSideTagLabel = computed(() =>
|
||||
supplementDraft.contracts.length ? '已选' : '待补充'
|
||||
)
|
||||
const supplementSideTagType = computed(() =>
|
||||
supplementDraft.contracts.length ? 'success' : 'warning'
|
||||
)
|
||||
|
||||
function asGapItem(row: PlPlanListDisplayRow): PlanContractGapItem {
|
||||
return row.plan as unknown as PlanContractGapItem
|
||||
}
|
||||
|
||||
/** 段内合同缺口:装货行展示供方,送货行展示需方 */
|
||||
/** 段内合同:优先段上 contracts;无则回退旧计划级供/需合同 */
|
||||
function getContractGapStopLines(row: PlPlanListDisplayRow) {
|
||||
if ((row.stop.contracts?.length ?? 0) > 0) {
|
||||
return getStopContractLines(row)
|
||||
@@ -557,16 +563,19 @@ function getContractGapStopLines(row: PlPlanListDisplayRow) {
|
||||
)
|
||||
}
|
||||
|
||||
function showGapSupplementAction(row: PlPlanListDisplayRow): boolean {
|
||||
const gap = asGapItem(row)
|
||||
if (row.kind === 'load') {
|
||||
return !gap.supplierContractExempt && (gap.gapSides || []).includes('supplier')
|
||||
}
|
||||
return !gap.demanderContractExempt && (gap.gapSides || []).includes('demander')
|
||||
function isStopContractExempt(row: PlPlanListDisplayRow): boolean {
|
||||
return isPlInternalCustomerName(getStopCustomerName(row))
|
||||
}
|
||||
|
||||
function gapExpandRowKey(planId: string) {
|
||||
return `${planId}-load`
|
||||
function stopHasContracts(row: PlPlanListDisplayRow): boolean {
|
||||
return getContractGapStopLines(row).some((line) => !!line.contractId && !line.pending)
|
||||
}
|
||||
|
||||
/** 仅非内置客户、且本段缺合同时显示「补充」 */
|
||||
function showGapSupplementAction(row: PlPlanListDisplayRow): boolean {
|
||||
if (isStopContractExempt(row)) return false
|
||||
if (!row.stop.id) return false
|
||||
return !stopHasContracts(row)
|
||||
}
|
||||
|
||||
function getContractGapExportFilterParams() {
|
||||
@@ -591,17 +600,17 @@ const contractGapExportColumns = [
|
||||
'gapType'
|
||||
]
|
||||
|
||||
|
||||
function cloneLinks(links?: PlanContractLink[]): PlanContractLink[] {
|
||||
return (links || []).map(item => ({
|
||||
return (links || []).map((item) => ({
|
||||
contractId: item.contractId,
|
||||
contractNo: item.contractNo
|
||||
contractNo: item.contractNo,
|
||||
stopId: item.stopId
|
||||
}))
|
||||
}
|
||||
|
||||
function linksFingerprint(links: PlanContractLink[]) {
|
||||
return cloneLinks(links)
|
||||
.map(item => item.contractId)
|
||||
.map((item) => item.contractId)
|
||||
.sort()
|
||||
.join(',')
|
||||
}
|
||||
@@ -615,116 +624,75 @@ function getPlanTypeLabel(type?: string | null) {
|
||||
return type ? map[type] || type : '—'
|
||||
}
|
||||
|
||||
function sideBoxClass(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const exempt = side === 'supplier' ? row.supplierContractExempt : row.demanderContractExempt
|
||||
if (exempt) return 'is-exempt'
|
||||
const missing = (row.gapSides || []).includes(side)
|
||||
return missing ? 'is-need' : 'is-ok'
|
||||
function resolveStopDraftContracts(row: PlPlanListDisplayRow): PlanContractLink[] {
|
||||
if ((row.stop.contracts?.length ?? 0) > 0) {
|
||||
return cloneLinks(row.stop.contracts)
|
||||
}
|
||||
const gap = asGapItem(row)
|
||||
if (row.kind === 'load') return cloneLinks(gap.supplierContracts)
|
||||
return cloneLinks(gap.demanderContracts)
|
||||
}
|
||||
|
||||
function sideTagLabel(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const exempt = side === 'supplier' ? row.supplierContractExempt : row.demanderContractExempt
|
||||
if (exempt) return '可免'
|
||||
return (row.gapSides || []).includes(side) ? '待补充' : '已齐'
|
||||
function isSupplementDirty() {
|
||||
return linksFingerprint(supplementDraft.contracts) !== supplementDraft.baseline
|
||||
}
|
||||
|
||||
function sideTagType(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const exempt = side === 'supplier' ? row.supplierContractExempt : row.demanderContractExempt
|
||||
if (exempt) return 'info'
|
||||
return (row.gapSides || []).includes(side) ? 'warning' : 'success'
|
||||
function resetSupplementDraft() {
|
||||
supplementRow.value = null
|
||||
supplementDraft.contracts = []
|
||||
supplementDraft.baseline = ''
|
||||
supplementDraft.stopType = 'load'
|
||||
supplementDraft.stopId = ''
|
||||
supplementDraft.planId = ''
|
||||
supplementDraft.rowKey = ''
|
||||
}
|
||||
|
||||
function isExpanded(id: string) {
|
||||
return expandedRowKeys.value.includes(gapExpandRowKey(id))
|
||||
function openSupplementDialog(row: PlPlanListDisplayRow) {
|
||||
if (!row.stop.id) {
|
||||
ElMessage.warning('该段次缺少段 ID,无法补充合同')
|
||||
return
|
||||
}
|
||||
if (isStopContractExempt(row)) {
|
||||
ElMessage.info('内置客户无需补充合同')
|
||||
return
|
||||
}
|
||||
const contracts = resolveStopDraftContracts(row)
|
||||
supplementRow.value = row
|
||||
supplementDraft.contracts = contracts
|
||||
supplementDraft.baseline = linksFingerprint(contracts)
|
||||
supplementDraft.stopType = row.kind
|
||||
supplementDraft.stopId = String(row.stop.id || '').trim()
|
||||
supplementDraft.planId = row.plan.id
|
||||
supplementDraft.rowKey = row.rowKey
|
||||
supplementVisible.value = true
|
||||
}
|
||||
|
||||
function ensureDraft(row: PlanContractGapItem) {
|
||||
if (expandDraft[row.id]) return
|
||||
const supplierContracts = cloneLinks(row.supplierContracts)
|
||||
const demanderContracts = cloneLinks(row.demanderContracts)
|
||||
expandDraft[row.id] = {
|
||||
supplierContracts,
|
||||
demanderContracts,
|
||||
baselineSupplier: linksFingerprint(supplierContracts),
|
||||
baselineDemander: linksFingerprint(demanderContracts)
|
||||
async function confirmDiscardIfDirty(): Promise<boolean> {
|
||||
if (!isSupplementDirty()) return true
|
||||
try {
|
||||
await ElMessageBox.confirm('当前有未保存的合同变更,确认关闭吗?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '关闭',
|
||||
cancelButtonText: '继续编辑'
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isDraftDirty(id: string) {
|
||||
const draft = expandDraft[id]
|
||||
if (!draft) return false
|
||||
return (
|
||||
linksFingerprint(draft.supplierContracts) !== draft.baselineSupplier ||
|
||||
linksFingerprint(draft.demanderContracts) !== draft.baselineDemander
|
||||
)
|
||||
async function requestCloseSupplement() {
|
||||
const ok = await confirmDiscardIfDirty()
|
||||
if (!ok) return
|
||||
supplementVisible.value = false
|
||||
resetSupplementDraft()
|
||||
}
|
||||
|
||||
async function collapseRow(id: string) {
|
||||
if (isDraftDirty(id)) {
|
||||
try {
|
||||
await ElMessageBox.confirm('当前有未保存的合同变更,确认收起吗?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '收起',
|
||||
cancelButtonText: '继续编辑'
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
delete expandDraft[id]
|
||||
expandedRowKeys.value = expandedRowKeys.value.filter((key) => key !== gapExpandRowKey(id))
|
||||
return true
|
||||
}
|
||||
|
||||
async function expandRow(row: PlanContractGapItem) {
|
||||
const currentKey = expandedRowKeys.value[0]
|
||||
const currentId = currentKey?.replace(/-load$/, '')
|
||||
if (currentId && currentId !== row.id) {
|
||||
const ok = await collapseRow(currentId)
|
||||
if (!ok) return
|
||||
}
|
||||
ensureDraft(row)
|
||||
expandedRowKeys.value = [gapExpandRowKey(row.id)]
|
||||
}
|
||||
|
||||
async function toggleExpand(row: PlanContractGapItem) {
|
||||
if (isExpanded(row.id)) {
|
||||
await collapseRow(row.id)
|
||||
} else {
|
||||
await expandRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExpandChange(
|
||||
displayRow: PlPlanListDisplayRow,
|
||||
expandedRows: PlPlanListDisplayRow[]
|
||||
) {
|
||||
const gap = asGapItem(displayRow)
|
||||
const willExpand = expandedRows.some((item) => item.rowKey === displayRow.rowKey)
|
||||
if (willExpand) {
|
||||
const currentKey = expandedRowKeys.value[0]
|
||||
const currentId = currentKey?.replace(/-load$/, '')
|
||||
if (currentId && currentId !== gap.id) {
|
||||
const ok = await collapseRow(currentId)
|
||||
if (!ok) {
|
||||
nextTick(() => {
|
||||
const firstRow = findPlanFirstDisplayRow(gap.id)
|
||||
if (firstRow) tableRef.value?.toggleRowExpansion?.(firstRow, false)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
ensureDraft(gap)
|
||||
expandedRowKeys.value = [gapExpandRowKey(gap.id)]
|
||||
} else if (expandedRowKeys.value.includes(gapExpandRowKey(gap.id))) {
|
||||
const ok = await collapseRow(gap.id)
|
||||
if (!ok) {
|
||||
nextTick(() => {
|
||||
const firstRow = findPlanFirstDisplayRow(gap.id)
|
||||
if (firstRow) tableRef.value?.toggleRowExpansion?.(firstRow, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
async function handleSupplementBeforeClose(done: () => void) {
|
||||
const ok = await confirmDiscardIfDirty()
|
||||
if (!ok) return
|
||||
resetSupplementDraft()
|
||||
done()
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -744,15 +712,6 @@ async function fetchData() {
|
||||
const data = (res as any).data ?? res
|
||||
tableData.value = data.list || []
|
||||
pagination.total = data.total || 0
|
||||
// 收起已不在列表中的展开行
|
||||
const ids = new Set(tableData.value.map((item) => item.id))
|
||||
expandedRowKeys.value = expandedRowKeys.value.filter((key) => {
|
||||
const planId = key.replace(/-load$/, '')
|
||||
return ids.has(planId)
|
||||
})
|
||||
Object.keys(expandDraft).forEach(id => {
|
||||
if (!ids.has(id)) delete expandDraft[id]
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -804,37 +763,37 @@ function handleView(row: PlanContractGapItem) {
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSaveSide(row: PlanContractGapItem, side: 'supplier' | 'demander') {
|
||||
const draft = expandDraft[row.id]
|
||||
if (!draft) return
|
||||
const contracts =
|
||||
side === 'supplier' ? draft.supplierContracts : draft.demanderContracts
|
||||
savingKey.value = `${row.id}:${side}`
|
||||
async function handleSaveStopContracts() {
|
||||
if (!supplementDraft.stopId) {
|
||||
ElMessage.warning('该段次缺少段 ID,无法保存')
|
||||
return
|
||||
}
|
||||
if (!supplementRow.value?.stop.customerId) {
|
||||
ElMessage.warning('请先维护本段客户后再选择合同')
|
||||
return
|
||||
}
|
||||
if (!supplementDraft.contracts.length) {
|
||||
ElMessage.warning('请先选择至少 1 份合同')
|
||||
return
|
||||
}
|
||||
supplementSaving.value = true
|
||||
try {
|
||||
const res = await planApi.updateSideContracts(row.id, side, contracts)
|
||||
const updated = ((res as any).data ?? res) as PlanContractGapItem
|
||||
ElMessage.success(side === 'supplier' ? '供方合同已保存' : '需方合同已保存')
|
||||
await planApi.updateStopContracts(supplementDraft.planId, {
|
||||
stopType: supplementDraft.stopType,
|
||||
stopId: supplementDraft.stopId,
|
||||
contracts: supplementDraft.contracts
|
||||
})
|
||||
ElMessage.success('本段合同已保存')
|
||||
|
||||
if (!(updated.gapSides || []).length) {
|
||||
tableData.value = tableData.value.filter(item => item.id !== row.id)
|
||||
pagination.total = Math.max(0, pagination.total - 1)
|
||||
delete expandDraft[row.id]
|
||||
expandedRowKeys.value = []
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
return
|
||||
}
|
||||
// 先对齐 baseline,避免关闭时被 before-close 当成未保存变更
|
||||
supplementDraft.baseline = linksFingerprint(supplementDraft.contracts)
|
||||
supplementVisible.value = false
|
||||
resetSupplementDraft()
|
||||
|
||||
const idx = tableData.value.findIndex(item => item.id === row.id)
|
||||
if (idx >= 0) {
|
||||
tableData.value[idx] = updated
|
||||
}
|
||||
draft.supplierContracts = cloneLinks(updated.supplierContracts)
|
||||
draft.demanderContracts = cloneLinks(updated.demanderContracts)
|
||||
draft.baselineSupplier = linksFingerprint(draft.supplierContracts)
|
||||
draft.baselineDemander = linksFingerprint(draft.demanderContracts)
|
||||
await fetchData()
|
||||
await refreshMenuBadges(menuStore.menuList)
|
||||
} finally {
|
||||
savingKey.value = ''
|
||||
supplementSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,21 +832,48 @@ onMounted(() => {
|
||||
border: 1px solid rgba(217, 119, 6, 0.2);
|
||||
}
|
||||
|
||||
.pl-contract-expand {
|
||||
padding: 12px 16px 16px;
|
||||
background: #f8fffb;
|
||||
border-top: 1px dashed #a7f3d0;
|
||||
.pl-contract-supplement-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
.pl-contract-expand__grid {
|
||||
.pl-contract-supplement-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pl-contract-supplement-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.pl-contract-expand__grid {
|
||||
grid-template-columns: 1fr;
|
||||
@media (max-width: 700px) {
|
||||
.pl-contract-supplement-summary {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-contract-supplement-kv {
|
||||
min-width: 0;
|
||||
|
||||
.k {
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.v {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text, #1e293b);
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,7 +881,7 @@ onMounted(() => {
|
||||
background: #fff;
|
||||
border: 1px solid var(--el-border-color, #d9e8e2);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.pl-contract-side.is-need {
|
||||
@@ -908,11 +894,6 @@ onMounted(() => {
|
||||
background: #f6fffb;
|
||||
}
|
||||
|
||||
.pl-contract-side.is-exempt {
|
||||
border-color: #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pl-contract-side__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -922,43 +903,16 @@ onMounted(() => {
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-contract-side__customer {
|
||||
margin: 0 0 10px;
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pl-contract-side__exempt {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary, #64748b);
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pl-contract-side__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.pl-contract-expand-col) {
|
||||
.cell {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.el-table__expand-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-gap-contract-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -483,6 +483,7 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
@@ -515,20 +516,10 @@
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="pl-plan-contract-cell">
|
||||
<div
|
||||
v-for="(line, index) in getStopContractLines(row)"
|
||||
:key="`${line.side}-${line.no}-${index}`"
|
||||
class="pl-plan-contract-cell__row"
|
||||
>
|
||||
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
|
||||
<span
|
||||
class="pl-plan-contract-cell__no"
|
||||
:class="{ 'is-pending': line.pending }"
|
||||
>{{ line.no }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<PlPlanContractCell
|
||||
:lines="getStopContractLines(row)"
|
||||
@open="openContractDetail"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -691,6 +682,8 @@
|
||||
</template>
|
||||
</PlPlanViewDialog>
|
||||
|
||||
<ContractDetailDialog ref="contractDetailRef" hide-product-price />
|
||||
|
||||
<DbPoundSlipQrcodeDialog
|
||||
v-model:visible="qrcodeVisible"
|
||||
:inventory-id="selectedInventoryId"
|
||||
@@ -797,6 +790,7 @@
|
||||
import PlDeliverySlipPrintDialog from '@/components/business/pl-delivery-slip-print-dialog/index.vue'
|
||||
import PlSubpackSlipPrintDialog from '@/components/business/pl-subpack-slip-print-dialog/index.vue'
|
||||
import PlPlanDeliveryCustomerSelectDialog from '@/components/business/pl-plan-delivery-customer-select-dialog/index.vue'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import PlPasteRecognizeDialog from '@/components/business/pl-paste-recognize-dialog/index.vue'
|
||||
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -804,6 +798,7 @@
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useContractDetailDialog } from '@/composables/useContractDetailDialog'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { usePlPlanArrangeActions } from '@/composables/usePlPlanArrangeActions'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
@@ -831,7 +826,7 @@
|
||||
getStopProducts,
|
||||
getStopTime,
|
||||
resolveStopCertProducts,
|
||||
sumStopActualQty,
|
||||
sumStopArchiveActualQty,
|
||||
sumStopProductsField,
|
||||
type PlPlanListDisplayRow
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
@@ -860,6 +855,7 @@
|
||||
] as const
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { ContractDetailDialog, contractDetailRef, openContractDetail } = useContractDetailDialog()
|
||||
|
||||
const loading = ref(false)
|
||||
const voidLoading = ref(false)
|
||||
@@ -1023,7 +1019,7 @@
|
||||
if (prop === 'productPlanQty') return sumStopProductsField(stopProducts, 'planQty')
|
||||
if (prop === 'productInboundQty') return sumStopProductsField(stopProducts, 'inboundQty')
|
||||
if (prop === 'productOutboundQty') return sumStopProductsField(stopProducts, 'outboundQty')
|
||||
if (prop === 'productActualQty') return sumStopActualQty(plan, stopProducts)
|
||||
if (prop === 'productActualQty') return sumStopArchiveActualQty(stopProducts, row.kind)
|
||||
if (prop === 'productName') {
|
||||
return stopProducts
|
||||
.map((p) => p.productName || '')
|
||||
|
||||
Reference in new issue
Block a user