提交
This commit is contained in:
1 parent
c8cec132bf
commit
19c5aaa8f5
95 files changed
+309129
-9838
No files matched your search
@@ -871,9 +871,17 @@ export interface ContractProductArrangeQtyPlanItem {
|
||||
planNo?: string
|
||||
planDate?: string | null
|
||||
planQty: number
|
||||
planType?: string | null
|
||||
belongsCompany?: string | null
|
||||
supplierName?: string | null
|
||||
customerName?: string | null
|
||||
licensePlate?: string | null
|
||||
driverName?: string | null
|
||||
followUserName?: string | null
|
||||
followStatus?: string
|
||||
transportStatus?: string
|
||||
archiveStatus?: string
|
||||
createTime?: string | null
|
||||
}
|
||||
|
||||
export interface ContractProductArrangeQtyDetail {
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
background-color: #ecfdf5 !important;
|
||||
}
|
||||
|
||||
tbody tr.pl-plan-list-row:hover > td.el-table__cell {
|
||||
background-color: var(--el-table-row-hover-bg-color, #f1f5f9) !important;
|
||||
/* 悬停用浅红;须压过斑马纹/装货段 !important 底色 */
|
||||
tbody tr.pl-plan-list-row:hover > td.el-table__cell,
|
||||
tbody tr.pl-plan-list-row.hover-row > td.el-table__cell {
|
||||
background-color: #fee2e2 !important;
|
||||
}
|
||||
|
||||
.pl-plan-list-row--plan-end > td {
|
||||
|
||||
@@ -470,6 +470,17 @@ html body .el-tag {
|
||||
padding: 25px 20px !important;
|
||||
}
|
||||
|
||||
/* 运输计划 · 合同数量校验/提醒:加宽以容纳产品明细表 */
|
||||
.el-message-box.pl-contract-qty-msgbox {
|
||||
width: 780px !important;
|
||||
max-width: 92vw !important;
|
||||
}
|
||||
|
||||
.el-message-box.pl-contract-qty-msgbox .el-message-box__message {
|
||||
max-height: min(60vh, 520px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.el-message-box__title {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<!-- 合同详情 · 关联运输计划列表(列与 /#/pl/plan 一致;壳层对齐 dialog-list-panel / OpenSpec) -->
|
||||
<template>
|
||||
<div class="pl-contract-linked-plans-panel panel dialog-list-panel">
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
共关联 <span class="sel-num">{{ pagination.total }}</span> 条运输计划
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll bx-list-table-wrap">
|
||||
<ListTable
|
||||
table-key="crm-contract-linked-plans"
|
||||
class="list-table pl-plan-merge-list-table"
|
||||
:data="displayRows"
|
||||
v-loading="loading"
|
||||
row-key="rowKey"
|
||||
:span-method="planListSpanMethod"
|
||||
:row-class-name="getPlPlanListRowClassName"
|
||||
empty-text="暂无关联运输计划"
|
||||
@row-dblclick="handleDisplayRowDblClick"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="getDisplayRowIndex"
|
||||
/>
|
||||
<el-table-column prop="planNo" label="计划编号" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleView(row.plan)">{{ row.plan.planNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="followStatus" label="跟进状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlPlanFollowStatusTagType(row.plan.followStatus)" size="small">
|
||||
{{ getPlPlanFollowStatusLabel(row.plan.followStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.plan.planDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planType" label="单据类型" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ getPlanTypeLabel(row.plan.planType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="licensePlate"
|
||||
label="车牌号"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatPlVehiclePlateDisplay(row.plan.licensePlate, row.plan.trailerNo) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="personnelSummary"
|
||||
label="人员"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ normalizePlPersonnelSummaryDisplay(row.plan.personnelSummary) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.plan.followUserName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="arrangeCopy"
|
||||
label="链接复制"
|
||||
min-width="88"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.plan.canCopyPoundQrcode" class="pl-plan-arrange-copy-cell">
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
title="复制磅单二维码"
|
||||
:loading="
|
||||
arrangeCopyLoadingId === row.plan.id && arrangeCopyLoadingAction === 'qrcode'
|
||||
"
|
||||
@click.stop="handleListCopyPoundQrcode(row.plan)"
|
||||
>
|
||||
磅单码
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopKind"
|
||||
label="段次"
|
||||
min-width="88"
|
||||
align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="pl-plan-stop-kind-text">{{ getStopKindLabel(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopCustomer"
|
||||
label="客户"
|
||||
min-width="200"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>{{ getStopCustomerName(row) }}</span>
|
||||
<el-tag
|
||||
v-if="isPlInternalCustomerName(getStopCustomerName(row))"
|
||||
type="success"
|
||||
size="small"
|
||||
class="pl-plan-internal-customer-tag"
|
||||
>
|
||||
内部
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopAddressTime"
|
||||
label="地址 / 时间"
|
||||
min-width="220"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="pl-plan-stop-address-time">
|
||||
<div class="pl-plan-stop-address-time__addr">{{ getStopAddress(row) }}</div>
|
||||
<div class="pl-plan-stop-address-time__time">{{ getStopTime(row) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="产品名称"
|
||||
min-width="160"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="name"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="型号/规格"
|
||||
min-width="96"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="spec"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="包装规格"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="packageSpec"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="单位"
|
||||
min-width="64"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="unit"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="计划数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="planQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="实际数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="计划附件"
|
||||
min-width="200"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListCertCell
|
||||
:products="resolveStopCertProducts(row.plan, getStopProducts(row))"
|
||||
compact
|
||||
hide-product
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
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)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="入库数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="inboundQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="出库数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="outboundQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopDibang"
|
||||
label="地磅"
|
||||
min-width="72"
|
||||
align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="formatStopDibangLabel(row.stop)"
|
||||
:type="getStopDibangTagType(row.stop)"
|
||||
size="small"
|
||||
>
|
||||
{{ formatStopDibangLabel(row.stop) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
</div>
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
共 <span class="sel-num">{{ pagination.total }}</span> 条
|
||||
</div>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="sizes, prev, pager, next"
|
||||
background
|
||||
@size-change="handlePageSizeChange"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PlPlanViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:plan-id="viewPlanId"
|
||||
show-inbound-outbound-qty
|
||||
/>
|
||||
|
||||
<DbPoundSlipQrcodeDialog
|
||||
ref="qrcodeDialogRef"
|
||||
v-model:visible="qrcodeVisible"
|
||||
:inventory-id="selectedInventoryId"
|
||||
:quantity-unit="selectedInventoryUnit"
|
||||
/>
|
||||
<PlPlanPoundQrcodePickDialog
|
||||
v-model:visible="poundQrcodePickVisible"
|
||||
:records="poundQrcodePickRecords"
|
||||
:plan="arrangeActionPlan"
|
||||
@copy="handlePoundQrcodePickCopy"
|
||||
@preview="handlePoundQrcodePickPreview"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
|
||||
import PlPlanPoundQrcodePickDialog from '@/components/business/pl-plan-pound-qrcode-pick-dialog/index.vue'
|
||||
import { usePlPlanMergeListTable } from '@/composables/usePlPlanMergeListTable'
|
||||
import { usePlPlanArrangeActions } from '@/composables/usePlPlanArrangeActions'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import {
|
||||
formatStopDibangLabel,
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopContractLines,
|
||||
getStopCustomerName,
|
||||
getStopDibangTagType,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime,
|
||||
resolveStopCertProducts
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
import {
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay
|
||||
} from '@/utils/pl-display'
|
||||
import { getPlanTypeLabel } from '@/composables/usePlContractToPlan'
|
||||
|
||||
const props = defineProps<{
|
||||
contractId?: string | null
|
||||
active?: boolean
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const planList = ref<Plan[]>([])
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
/** 与 pl/plan 列表 PLAN_MERGE_COLUMN_PROPS 顺序一致(含勾选/序号/计划编号固定 3 列) */
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
'followStatus',
|
||||
'planDate',
|
||||
'planType',
|
||||
'licensePlate',
|
||||
'personnelSummary',
|
||||
'followUserName',
|
||||
'arrangeCopy'
|
||||
] as const
|
||||
|
||||
const visiblePlanMergeProps = computed(() => [...PLAN_MERGE_COLUMN_PROPS])
|
||||
|
||||
const {
|
||||
displayRows,
|
||||
planListSpanMethod,
|
||||
getDisplayRowIndex,
|
||||
handleDisplayRowDblClick
|
||||
} = usePlPlanMergeListTable({
|
||||
planList,
|
||||
visiblePlanMergeProps,
|
||||
selectedRows,
|
||||
pagination,
|
||||
onView: handleView,
|
||||
fixedMergeColCount: 2
|
||||
})
|
||||
|
||||
const {
|
||||
plan: arrangeActionPlan,
|
||||
qrcodeVisible,
|
||||
selectedInventoryId,
|
||||
selectedInventoryUnit,
|
||||
qrcodeDialogRef,
|
||||
poundQrcodePickVisible,
|
||||
poundQrcodePickRecords,
|
||||
loadArrangeActions,
|
||||
copyPoundSlipQrcode,
|
||||
handlePoundQrcodePickCopy,
|
||||
handlePoundQrcodePickPreview
|
||||
} = usePlPlanArrangeActions()
|
||||
|
||||
const arrangeCopyLoadingId = ref('')
|
||||
const arrangeCopyLoadingAction = ref<'qrcode' | ''>('')
|
||||
|
||||
async function handleListCopyPoundQrcode(row: Plan) {
|
||||
if (!row.id || arrangeCopyLoadingId.value) return
|
||||
arrangeCopyLoadingId.value = row.id
|
||||
arrangeCopyLoadingAction.value = 'qrcode'
|
||||
try {
|
||||
await loadArrangeActions(row.id)
|
||||
await copyPoundSlipQrcode()
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请稍后重试')
|
||||
} finally {
|
||||
arrangeCopyLoadingId.value = ''
|
||||
arrangeCopyLoadingAction.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
const cid = String(props.contractId || '').trim()
|
||||
if (!cid) {
|
||||
planList.value = []
|
||||
pagination.total = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await planApi.list({
|
||||
contractId: cid,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = (res as any)?.data ?? res
|
||||
planList.value = data?.list || []
|
||||
pagination.total = data?.total || 0
|
||||
} catch {
|
||||
planList.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageSizeChange() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleView(plan: Plan) {
|
||||
if (!plan?.id) return
|
||||
viewPlanId.value = plan.id
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.contractId, props.active] as const,
|
||||
([cid, active]) => {
|
||||
if (!active || !cid) return
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: fetchData })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@styles/business/pl-plan-merge-list-table.scss';
|
||||
|
||||
.pl-contract-linked-plans-panel {
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-plan-arrange-copy-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pl-plan-arrange-copy-cell .btn-ghost.el-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+398
-42
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="pl-contract-product-arrange-qty-host">
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
class="dialog-form-layout pl-contract-product-arrange-qty-dialog"
|
||||
:title="dialogTitle"
|
||||
width="640px"
|
||||
width="min(2000px, calc(100vw - 32px))"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -26,25 +27,274 @@
|
||||
<div class="v is-avail">{{ availableQtyText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrange-qty-list-title">已安排本产品的运输计划</div>
|
||||
<el-table
|
||||
v-if="(detail?.plans || []).length"
|
||||
:data="detail?.plans || []"
|
||||
class="list-table"
|
||||
stripe
|
||||
size="small"
|
||||
max-height="280"
|
||||
>
|
||||
<el-table-column prop="planNo" label="计划编号" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="planDate" label="计划日期" min-width="110" />
|
||||
<el-table-column label="计划数量" min-width="100" align="right">
|
||||
<template #default="{ row }">{{ formatQty(row.planQty) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">{{ statusLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p v-else class="arrange-qty-empty">暂无已安排计划</p>
|
||||
|
||||
<div class="panel dialog-list-panel arrange-qty-plan-panel">
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已安排本产品的运输计划
|
||||
<template v-if="arrangedPlanCount">
|
||||
· 共 <span class="sel-num">{{ arrangedPlanCount }}</span> 条
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll bx-list-table-wrap">
|
||||
<ListTable
|
||||
v-if="displayRows.length"
|
||||
table-key="pl-contract-product-arrange-qty-plans"
|
||||
class="list-table pl-plan-merge-list-table"
|
||||
:data="displayRows"
|
||||
row-key="rowKey"
|
||||
:span-method="planListSpanMethod"
|
||||
:row-class-name="getPlPlanListRowClassName"
|
||||
empty-text="暂无已安排计划"
|
||||
@row-dblclick="handleDisplayRowDblClick"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="getDisplayRowIndex"
|
||||
/>
|
||||
<el-table-column prop="planNo" label="计划编号" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleView(row.plan)">{{
|
||||
row.plan.planNo
|
||||
}}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="followStatus" label="跟进状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlPlanFollowStatusTagType(row.plan.followStatus)" size="small">
|
||||
{{ getPlPlanFollowStatusLabel(row.plan.followStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.plan.planDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planType" label="单据类型" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ getPlanTypeLabel(row.plan.planType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="licensePlate"
|
||||
label="车牌号"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatPlVehiclePlateDisplay(row.plan.licensePlate, row.plan.trailerNo) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="personnelSummary"
|
||||
label="人员"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ normalizePlPersonnelSummaryDisplay(row.plan.personnelSummary) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopKind"
|
||||
label="段次"
|
||||
min-width="88"
|
||||
align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="pl-plan-stop-kind-text">{{ getStopKindLabel(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopCustomer"
|
||||
label="客户"
|
||||
min-width="200"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>{{ getStopCustomerName(row) }}</span>
|
||||
<el-tag
|
||||
v-if="isPlInternalCustomerName(getStopCustomerName(row))"
|
||||
type="success"
|
||||
size="small"
|
||||
class="pl-plan-internal-customer-tag"
|
||||
>
|
||||
内部
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopAddressTime"
|
||||
label="地址 / 时间"
|
||||
min-width="220"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="pl-plan-stop-address-time">
|
||||
<div class="pl-plan-stop-address-time__addr">{{ getStopAddress(row) }}</div>
|
||||
<div class="pl-plan-stop-address-time__time">{{ getStopTime(row) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="产品名称"
|
||||
min-width="160"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap pl-plan-stop-col"
|
||||
label-class-name="col-name-wrap"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="name"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="型号/规格"
|
||||
min-width="96"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="spec"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="包装规格"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="packageSpec"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="单位"
|
||||
min-width="64"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="unit"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="计划数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="planQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="实际数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell
|
||||
:stop-kind="row.kind"
|
||||
:stop-products="getStopProducts(row)"
|
||||
:plan-products="row.plan.products"
|
||||
field="actualQty"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="计划附件"
|
||||
min-width="200"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListCertCell
|
||||
:products="resolveStopCertProducts(row.plan, getStopProducts(row))"
|
||||
compact
|
||||
hide-product
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
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)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="stopDibang"
|
||||
label="地磅"
|
||||
min-width="72"
|
||||
align="center"
|
||||
class-name="pl-plan-stop-col"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="formatStopDibangLabel(row.stop)"
|
||||
:type="getStopDibangTagType(row.stop)"
|
||||
size="small"
|
||||
>
|
||||
{{ formatStopDibangLabel(row.stop) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<p v-else class="arrange-qty-empty">暂无已安排计划</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="emit('update:modelValue', false)">
|
||||
@@ -52,12 +302,53 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<PlPlanViewDialog
|
||||
:visible="viewVisible"
|
||||
:plan-id="viewPlanId"
|
||||
show-inbound-outbound-qty
|
||||
@update:visible="viewVisible = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check } from '@element-plus/icons-vue'
|
||||
import { planApi, type ContractProductArrangeQtyDetail } from '@/api/pl'
|
||||
import {
|
||||
planApi,
|
||||
type ContractProductArrangeQtyDetail,
|
||||
type Plan
|
||||
} from '@/api/pl'
|
||||
import ListTable from '@/components/business/list-table/index.vue'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
import PlPlanContractCell from '@/components/business/pl-plan-contract-cell/index.vue'
|
||||
import { getPlanTypeLabel } from '@/composables/usePlContractToPlan'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import {
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay
|
||||
} from '@/utils/pl-display'
|
||||
import {
|
||||
buildPlPlanListSpanMethod,
|
||||
flattenPlanListRowsForProduct,
|
||||
formatStopDibangLabel,
|
||||
getPlanListDisplayIndex,
|
||||
getPlPlanListRowClassName,
|
||||
getStopAddress,
|
||||
getStopContractLines,
|
||||
getStopCustomerName,
|
||||
getStopDibangTagType,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime,
|
||||
resolveStopCertProducts,
|
||||
type PlPlanListDisplayRow
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -71,6 +362,30 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<ContractProductArrangeQtyDetail | null>(null)
|
||||
const planList = ref<Plan[]>([])
|
||||
const viewVisible = ref(false)
|
||||
const viewPlanId = ref('')
|
||||
|
||||
/** 与运输计划列表计划区合并列一致(本弹窗无勾选/链接复制) */
|
||||
const PLAN_MERGE_COLUMN_PROPS = [
|
||||
'followStatus',
|
||||
'planDate',
|
||||
'planType',
|
||||
'licensePlate',
|
||||
'personnelSummary'
|
||||
] as const
|
||||
|
||||
const productIdRef = computed(() => String(props.productId || detail.value?.productId || '').trim())
|
||||
|
||||
const displayRows = computed(() =>
|
||||
flattenPlanListRowsForProduct(planList.value, productIdRef.value)
|
||||
)
|
||||
|
||||
const planListSpanMethod = computed(() =>
|
||||
buildPlPlanListSpanMethod([...PLAN_MERGE_COLUMN_PROPS], 2)
|
||||
)
|
||||
|
||||
const arrangedPlanCount = computed(() => (detail.value?.plans || []).length)
|
||||
|
||||
const dialogTitle = computed(
|
||||
() => `合同用量 · ${props.productName || detail.value?.productName || '产品'}`
|
||||
@@ -97,13 +412,18 @@
|
||||
return Number(n).toFixed(3)
|
||||
}
|
||||
|
||||
function statusLabel(row: { archiveStatus?: string; followStatus?: string; transportStatus?: string }) {
|
||||
const archive = (row.archiveStatus || '').trim()
|
||||
if (archive === 'archived') return '已归档'
|
||||
const follow = (row.followStatus || '').trim()
|
||||
if (follow === 'completed') return '已完成'
|
||||
if (follow === 'arranged' || row.transportStatus === 'arranged') return '已安排'
|
||||
return follow || row.transportStatus || '—'
|
||||
function getDisplayRowIndex(index: number) {
|
||||
return getPlanListDisplayIndex(displayRows.value, index, 1, displayRows.value.length || 1)
|
||||
}
|
||||
|
||||
function handleView(plan: Plan) {
|
||||
if (!plan?.id) return
|
||||
viewPlanId.value = plan.id
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleDisplayRowDblClick(row: PlPlanListDisplayRow) {
|
||||
if (row?.plan) handleView(row.plan)
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
@@ -111,6 +431,7 @@
|
||||
const productId = String(props.productId || '').trim()
|
||||
if (!contractId || !productId) {
|
||||
detail.value = null
|
||||
planList.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
@@ -120,9 +441,29 @@
|
||||
productId,
|
||||
excludePlanId: props.excludePlanId || undefined
|
||||
})
|
||||
detail.value = pickBxData<ContractProductArrangeQtyDetail>(res) || null
|
||||
const data = pickBxData<ContractProductArrangeQtyDetail>(res) || null
|
||||
detail.value = data
|
||||
|
||||
const orderedIds = (data?.plans || [])
|
||||
.map((p) => String(p.planId || '').trim())
|
||||
.filter(Boolean)
|
||||
if (!orderedIds.length) {
|
||||
planList.value = []
|
||||
return
|
||||
}
|
||||
const idSet = new Set(orderedIds)
|
||||
const listRes = await planApi.list({
|
||||
contractId,
|
||||
page: 1,
|
||||
pageSize: 500
|
||||
})
|
||||
const listData = (listRes as any)?.data ?? listRes
|
||||
const all: Plan[] = listData?.list || []
|
||||
const byId = new Map(all.filter((p) => p?.id && idSet.has(p.id)).map((p) => [p.id, p]))
|
||||
planList.value = orderedIds.map((id) => byId.get(id)).filter(Boolean) as Plan[]
|
||||
} catch {
|
||||
detail.value = null
|
||||
planList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -137,8 +478,10 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '@styles/business/pl-plan-merge-list-table.scss';
|
||||
|
||||
.arrange-qty-body {
|
||||
min-height: 120px;
|
||||
min-height: 420px;
|
||||
}
|
||||
.arrange-qty-sub {
|
||||
margin: 0 0 12px;
|
||||
@@ -148,21 +491,21 @@
|
||||
.arrange-qty-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.arrange-qty-stat {
|
||||
padding: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
|
||||
background: #f8fafc;
|
||||
.k {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
.v {
|
||||
margin-top: 2px;
|
||||
font-size: 15px;
|
||||
margin-top: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
&.is-avail {
|
||||
@@ -170,14 +513,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.arrange-qty-list-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
.arrange-qty-plan-panel {
|
||||
margin-top: 0;
|
||||
}
|
||||
.arrange-qty-empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.pl-contract-product-arrange-qty-dialog.dialog-form-layout {
|
||||
width: min(2000px, calc(100vw - 32px)) !important;
|
||||
max-width: min(2000px, calc(100vw - 32px));
|
||||
|
||||
.el-dialog__body {
|
||||
max-height: calc(100vh - 140px);
|
||||
min-height: 480px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,6 +33,7 @@
|
||||
:customer-name="meta.customerName"
|
||||
:generate-dibang="generateDibang"
|
||||
:dibang-rows="dibangRows"
|
||||
:plan-product-rows="products"
|
||||
:driver-feedback="driverFeedback"
|
||||
:site-feedback="siteFeedback"
|
||||
:fallback-remark="fallbackRemark"
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
class="pl-stop-feedback-dibang-item"
|
||||
:class="{ 'pl-stop-feedback-dibang-item--split': dibangRows.length > 1 }"
|
||||
>
|
||||
<p v-if="dibangRows.length > 1" class="pl-stop-feedback-ticket__product">
|
||||
<p class="pl-stop-feedback-ticket__product">
|
||||
{{ dibangProductLabel(row, idx) }}
|
||||
</p>
|
||||
<div class="pl-stop-feedback-ticket__qty pl-stop-feedback-ticket__qty--compact">
|
||||
@@ -219,6 +219,7 @@
|
||||
import type { TransportStopFeedback } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { formatKgToTon } from '@/components/business/db/db-related-view'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import type { PlPlanTransportDibangRow } from '@/composables/usePlPlanViewDetail'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
@@ -229,6 +230,8 @@
|
||||
customerName?: string
|
||||
generateDibang?: number
|
||||
dibangRows?: PlPlanTransportDibangRow[]
|
||||
/** 用于解析 CRM 标准产品名(库内 link.productName 可能是别名) */
|
||||
planProductRows?: PlanFormProductRow[]
|
||||
driverFeedback?: TransportStopFeedback | null
|
||||
siteFeedback?: TransportStopFeedback | null
|
||||
/** 段级无备注时回退展示(如运输单级司机回传备注) */
|
||||
@@ -245,6 +248,7 @@
|
||||
customerName: '',
|
||||
generateDibang: 0,
|
||||
dibangRows: () => [],
|
||||
planProductRows: () => [],
|
||||
driverFeedback: null,
|
||||
siteFeedback: null,
|
||||
fallbackRemark: '',
|
||||
@@ -388,8 +392,37 @@
|
||||
return { qty: String(qty), unit: ` ${unit}` }
|
||||
}
|
||||
|
||||
function resolvePlanProductRow(row: PlPlanTransportDibangRow) {
|
||||
const id = String(row.link.planProductId || '').trim()
|
||||
if (!id) return undefined
|
||||
return (props.planProductRows || []).find((p) => String(p.id || '').trim() === id)
|
||||
}
|
||||
|
||||
function dibangProductLabel(row: PlPlanTransportDibangRow, idx: number): string {
|
||||
return row.link.productName || row.inventory?.product || `地磅 ${idx + 1}`
|
||||
const planRow = resolvePlanProductRow(row)
|
||||
// 优先 CRM 标准名;link.productName / planRow.productName 可能是别名
|
||||
const product = (
|
||||
String(planRow?.standardProductName || '').trim() ||
|
||||
row.inventory?.product ||
|
||||
row.weighing?.cargoName ||
|
||||
String(planRow?.productName || '').trim() ||
|
||||
row.link.productName ||
|
||||
''
|
||||
).trim()
|
||||
const specification = (
|
||||
String(planRow?.model || '').trim() ||
|
||||
row.inventory?.specification ||
|
||||
row.weighing?.specification ||
|
||||
''
|
||||
).trim()
|
||||
if (!product && !specification) return `地磅 ${idx + 1}`
|
||||
if (!specification) return product
|
||||
if (!product) return specification
|
||||
if (product.endsWith(specification)) {
|
||||
const base = product.slice(0, -specification.length).trimEnd()
|
||||
return base ? `${base} ${specification}` : specification
|
||||
}
|
||||
return `${product} ${specification}`
|
||||
}
|
||||
|
||||
function dibangBatchNo(row: PlPlanTransportDibangRow): string {
|
||||
@@ -492,6 +525,11 @@
|
||||
&--dibang {
|
||||
background: linear-gradient(90deg, #fff7ed 0%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
// 标题栏与正文第一行间距
|
||||
& + * {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-stop-feedback-ticket__who {
|
||||
@@ -513,7 +551,7 @@
|
||||
|
||||
.pl-stop-feedback-ticket__product {
|
||||
margin: 0;
|
||||
padding: 8px 10px 0;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
@@ -600,7 +638,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 10px 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.pl-stop-feedback-ticket__product-row {
|
||||
@@ -644,7 +682,7 @@
|
||||
}
|
||||
|
||||
.pl-stop-feedback-ticket__empty {
|
||||
padding: 12px 10px;
|
||||
padding: 7px 10px 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-stop-product-dual-item__row2">
|
||||
<div class="pl-stop-product-dual-item__field">
|
||||
<!-- 可安排数量仅运输计划新增/编辑提醒用,详情弹窗不展示 -->
|
||||
<div v-if="showAvailableQty" class="pl-stop-product-dual-item__field">
|
||||
<span class="pl-stop-product-dual-item__label">可安排数量</span>
|
||||
<div class="pl-stop-avail-wrap">
|
||||
<span class="pl-stop-avail-num">{{ formatAvailableDisplay(row) }}</span>
|
||||
@@ -276,7 +277,12 @@
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可安排数量" min-width="120" align="right">
|
||||
<el-table-column
|
||||
v-if="showAvailableQty"
|
||||
label="可安排数量"
|
||||
min-width="120"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="pl-stop-avail-wrap pl-stop-avail-wrap--table">
|
||||
<span class="pl-stop-avail-num">{{ formatAvailableDisplay(row) }}</span>
|
||||
@@ -419,6 +425,7 @@
|
||||
/>
|
||||
|
||||
<PlContractProductArrangeQtyDialog
|
||||
v-if="showAvailableQty"
|
||||
v-model="arrangeQtyDialogVisible"
|
||||
:contract-id="arrangeQtyContractId"
|
||||
:product-id="arrangeQtyProductId"
|
||||
@@ -493,6 +500,8 @@
|
||||
const canRemove = computed(
|
||||
() => !!props.editable && (props.allowRemove === undefined ? true : props.allowRemove)
|
||||
)
|
||||
/** 仅新增/编辑表单展示可安排数量与问号;详情只读不展示 */
|
||||
const showAvailableQty = computed(() => !!props.editable)
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
@@ -574,11 +583,13 @@
|
||||
watch(
|
||||
() =>
|
||||
[
|
||||
props.editable,
|
||||
props.contractId,
|
||||
props.excludePlanId,
|
||||
(props.products || []).map((p) => p.productId).join(',')
|
||||
] as const,
|
||||
() => {
|
||||
if (!props.editable) return
|
||||
const cid = String(props.contractId || '').trim()
|
||||
if (!cid) return
|
||||
for (const row of props.products || []) {
|
||||
|
||||
@@ -324,14 +324,8 @@
|
||||
:key="row.link.dbInventoryId || idx"
|
||||
class="pl-feedback-ticket-qty-block"
|
||||
>
|
||||
<p
|
||||
v-if="dibangPlanRows.length > 1"
|
||||
class="pl-feedback-ticket-product"
|
||||
>
|
||||
<p class="pl-feedback-ticket-product">
|
||||
{{ dibangProductLabel(row, idx) }}
|
||||
<template v-if="dibangBatchNo(row) !== '—'">
|
||||
· {{ dibangBatchNo(row) }}
|
||||
</template>
|
||||
</p>
|
||||
<div class="pl-feedback-ticket-qty">
|
||||
<div>
|
||||
@@ -716,8 +710,37 @@
|
||||
]
|
||||
}
|
||||
|
||||
function resolvePlanProductRow(row: PlPlanTransportDibangRow) {
|
||||
const id = String(row.link.planProductId || '').trim()
|
||||
if (!id) return undefined
|
||||
return (props.planProductRows || []).find((p) => String(p.id || '').trim() === id)
|
||||
}
|
||||
|
||||
function dibangProductLabel(row: PlPlanTransportDibangRow, idx: number): string {
|
||||
return row.link.productName || row.inventory?.product || `地磅 ${idx + 1}`
|
||||
const planRow = resolvePlanProductRow(row)
|
||||
// 优先 CRM 标准名;link.productName / planRow.productName 可能是别名
|
||||
const product = (
|
||||
String(planRow?.standardProductName || '').trim() ||
|
||||
row.inventory?.product ||
|
||||
row.weighing?.cargoName ||
|
||||
String(planRow?.productName || '').trim() ||
|
||||
row.link.productName ||
|
||||
''
|
||||
).trim()
|
||||
const specification = (
|
||||
String(planRow?.model || '').trim() ||
|
||||
row.inventory?.specification ||
|
||||
row.weighing?.specification ||
|
||||
''
|
||||
).trim()
|
||||
if (!product && !specification) return `地磅 ${idx + 1}`
|
||||
if (!specification) return product
|
||||
if (!product) return specification
|
||||
if (product.endsWith(specification)) {
|
||||
const base = product.slice(0, -specification.length).trimEnd()
|
||||
return base ? `${base} ${specification}` : specification
|
||||
}
|
||||
return `${product} ${specification}`
|
||||
}
|
||||
|
||||
function dibangBatchNo(row: PlPlanTransportDibangRow): string {
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
:customer-name="loadView.customerName"
|
||||
:generate-dibang="loadView.generateDibang"
|
||||
:dibang-rows="loadDibangRows"
|
||||
:plan-product-rows="productRows"
|
||||
:driver-feedback="resolveDriverStopFeedback('load', loadStopId)"
|
||||
:site-feedback="resolveSiteStopFeedback('load', loadStopId)"
|
||||
:fallback-remark="transport?.feedbackRemark"
|
||||
@@ -215,6 +216,7 @@
|
||||
:customer-name="delivery.customerName"
|
||||
:generate-dibang="delivery.generateDibang"
|
||||
:dibang-rows="resolveDeliveryDibangRows(delivery)"
|
||||
:plan-product-rows="productRows"
|
||||
:driver-feedback="resolveDriverStopFeedback('delivery', delivery.id || '')"
|
||||
:site-feedback="resolveSiteStopFeedback('delivery', delivery.id || '')"
|
||||
:fallback-remark="transport?.feedbackRemark"
|
||||
|
||||
@@ -13,11 +13,16 @@ export function usePlPlanMergeListTable(options: {
|
||||
selectedRows: Ref<Plan[]>
|
||||
pagination: { page: number; pageSize: number }
|
||||
onView?: (plan: Plan) => void
|
||||
/** 计划区固定合并列数:默认 3(勾选+序号+计划编号);无勾选列时传 2 */
|
||||
fixedMergeColCount?: number
|
||||
}) {
|
||||
const displayRows = computed(() => flattenPlanListRows(unref(options.planList)))
|
||||
|
||||
const planListSpanMethod = computed(() =>
|
||||
buildPlPlanListSpanMethod([...options.visiblePlanMergeProps.value])
|
||||
buildPlPlanListSpanMethod(
|
||||
[...options.visiblePlanMergeProps.value],
|
||||
options.fixedMergeColCount
|
||||
)
|
||||
)
|
||||
|
||||
function getDisplayRowIndex(index: number) {
|
||||
|
||||
@@ -18,7 +18,8 @@ import { LanguageEnum } from '@/enums/appEnum'
|
||||
import { router } from '@/router'
|
||||
import { useWorktabStore } from './worktab'
|
||||
import { useMenuFavoriteStore } from './menuFavorite'
|
||||
import { clearFavoritePreopenFlag } from '@/utils/navigation/preopen-favorites'
|
||||
import { clearFavoritePreopenFlag } from '@/utils/navigation/preopen-favorites'
|
||||
import { clearPortalHomeActiveModule } from '@/utils/navigation/portal-home-active-module'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { resetRouterState } from '@/router/guards/beforeEach'
|
||||
import { useMenuStore } from './menu'
|
||||
@@ -156,6 +157,7 @@ export const useUserStore = defineStore(
|
||||
useMenuBadgeStore().clearCounts()
|
||||
useMenuFavoriteStore().clear()
|
||||
clearFavoritePreopenFlag()
|
||||
clearPortalHomeActiveModule()
|
||||
// 重置路由状态
|
||||
resetRouterState(500)
|
||||
// 跳转到登录页,携带当前路由作为 redirect 参数
|
||||
@@ -194,6 +196,7 @@ export const useUserStore = defineStore(
|
||||
worktabStore.keepAliveExclude = []
|
||||
useMenuFavoriteStore().clear()
|
||||
clearFavoritePreopenFlag()
|
||||
clearPortalHomeActiveModule()
|
||||
}
|
||||
|
||||
// 清除临时存储
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/** 门户首页侧栏选中模块(会话级,关标签页清空) */
|
||||
const STORAGE_KEY = 'portal-home-active-module'
|
||||
|
||||
export function getPortalHomeActiveModule(): string {
|
||||
try {
|
||||
return String(sessionStorage.getItem(STORAGE_KEY) || '').trim()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function setPortalHomeActiveModule(key: string): void {
|
||||
const v = String(key || '').trim()
|
||||
if (!v) return
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, v)
|
||||
} catch {
|
||||
/* ignore quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearPortalHomeActiveModule(): void {
|
||||
try {
|
||||
sessionStorage.removeItem(STORAGE_KEY)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,37 @@ import type { ContractQtyReminder, ContractQtyReminderItem } from '@/api/pl'
|
||||
|
||||
export const CONTRACT_QTY_VIOLATION_CODE = 'CONTRACT_QTY_OVERFLOW'
|
||||
|
||||
/** 弹窗明细行(超量 / 提醒共用) */
|
||||
export type ContractQtyDialogRow = {
|
||||
contractId?: string
|
||||
contractNo?: string
|
||||
customerName?: string
|
||||
stopKind?: string
|
||||
productId?: string
|
||||
productName?: string
|
||||
model?: string
|
||||
unit?: string
|
||||
contractQty?: number | null
|
||||
arrangedTotalQty?: number | null
|
||||
remainingQty?: number | null
|
||||
/** framework 无固定量 */
|
||||
framework?: boolean
|
||||
overflow?: boolean
|
||||
}
|
||||
|
||||
function formatQty(n: number | undefined | null): string {
|
||||
if (n == null || Number.isNaN(Number(n))) return '—'
|
||||
return Number(n).toFixed(3)
|
||||
}
|
||||
|
||||
function escapeHtml(raw: string): string {
|
||||
return String(raw)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function stopKindLabel(kind?: string): string {
|
||||
if (kind === 'delivery') return '送货'
|
||||
if (kind === 'load') return '装货'
|
||||
@@ -32,60 +58,142 @@ export function extractContractQtyViolationDetail(error: unknown): {
|
||||
}
|
||||
}
|
||||
|
||||
export function formatContractQtyViolationMessage(
|
||||
items: Array<Record<string, unknown>>
|
||||
): string {
|
||||
if (!items.length) return '合同产品安排数量超过合同数量,请调整后再试。'
|
||||
const lines = items.map((it) => {
|
||||
const name = String(it.productName || it.productId || '产品')
|
||||
const contractNo = String(it.contractNo || it.contractId || '')
|
||||
const side = stopKindLabel(String(it.stopKind || ''))
|
||||
const contractQty = formatQty(Number(it.contractQty))
|
||||
const effective = formatQty(Number(it.effectiveTotal ?? it.arrangedTotalQty))
|
||||
const overflow = formatQty(Number(it.overflowQty))
|
||||
const meta = [contractNo, side].filter(Boolean).join(' · ')
|
||||
return `「${name}」安排后合计 ${effective},超过合同数量 ${contractQty}(超 ${overflow})${meta ? `;${meta}` : ''}`
|
||||
function normalizeDialogRows(
|
||||
items: Array<Record<string, unknown> | ContractQtyReminderItem>
|
||||
): ContractQtyDialogRow[] {
|
||||
return (items || []).map((raw) => {
|
||||
const it = raw as Record<string, unknown>
|
||||
const mode = String(it.mode || '')
|
||||
const overflowQty = it.overflowQty != null ? Number(it.overflowQty) : null
|
||||
const remaining =
|
||||
it.remainingQty != null
|
||||
? Number(it.remainingQty)
|
||||
: it.effectiveTotal != null && it.contractQty != null
|
||||
? Number(it.contractQty) - Number(it.effectiveTotal)
|
||||
: null
|
||||
return {
|
||||
contractId: String(it.contractId || ''),
|
||||
contractNo: it.contractNo != null ? String(it.contractNo) : '',
|
||||
customerName: it.customerName != null ? String(it.customerName) : '',
|
||||
stopKind: it.stopKind != null ? String(it.stopKind) : '',
|
||||
productId: it.productId != null ? String(it.productId) : '',
|
||||
productName: it.productName != null ? String(it.productName) : '',
|
||||
model: it.model != null ? String(it.model) : '',
|
||||
unit: it.unit != null ? String(it.unit) : '',
|
||||
contractQty: it.contractQty != null ? Number(it.contractQty) : null,
|
||||
arrangedTotalQty:
|
||||
it.arrangedTotalQty != null
|
||||
? Number(it.arrangedTotalQty)
|
||||
: it.effectiveTotal != null
|
||||
? Number(it.effectiveTotal)
|
||||
: null,
|
||||
remainingQty: remaining,
|
||||
framework: mode === 'framework_cumulative',
|
||||
overflow: overflowQty != null ? overflowQty > 0 : remaining != null && remaining < 0
|
||||
}
|
||||
})
|
||||
return `以下产品「已安排合计 + 本次安排」超过合同数量,请调整后再试:\n\n${lines.join('\n')}`
|
||||
}
|
||||
|
||||
export function formatContractQtyReminderMessage(reminder: ContractQtyReminder): string {
|
||||
const items = reminder?.items || []
|
||||
if (!items.length) return ''
|
||||
const remaining = items.filter((i) => i.mode === 'remaining')
|
||||
const framework = items.filter((i) => i.mode === 'framework_cumulative')
|
||||
const blocks: string[] = []
|
||||
|
||||
if (remaining.length) {
|
||||
const lines = remaining.map((it: ContractQtyReminderItem) => {
|
||||
const name = it.productName || it.productId
|
||||
const meta = [it.contractNo, stopKindLabel(it.stopKind)].filter(Boolean).join(' · ')
|
||||
return `「${name}」已安排合计(含本次)${formatQty(it.arrangedTotalQty)},剩余可安排 ${formatQty(it.remainingQty)}${meta ? `;${meta}` : ''}`
|
||||
})
|
||||
blocks.push(`合同数量校验通过。各产品剩余可安排量:\n${lines.join('\n')}`)
|
||||
}
|
||||
|
||||
if (framework.length) {
|
||||
const lines = framework.map((it: ContractQtyReminderItem) => {
|
||||
const name = it.productName || it.productId
|
||||
const meta = [it.contractNo, stopKindLabel(it.stopKind)].filter(Boolean).join(' · ')
|
||||
return `「${name}」累计已安排 ${formatQty(it.arrangedTotalQty)}(本次 ${formatQty(it.thisPlanQty)})${meta ? `;${meta}` : ''}`
|
||||
})
|
||||
blocks.push(
|
||||
`关联框架合同无固定数量上限。本次安排后累计已安排:\n${lines.join('\n')}`
|
||||
)
|
||||
}
|
||||
|
||||
return blocks.join('\n\n')
|
||||
function groupKey(row: ContractQtyDialogRow): string {
|
||||
return `${row.contractId || row.contractNo || ''}::${row.stopKind || ''}`
|
||||
}
|
||||
|
||||
/** @returns true 表示已按合同超量处理(调用方勿再抛出/重复提示) */
|
||||
function groupTitle(row: ContractQtyDialogRow): string {
|
||||
const customer = (row.customerName || '').trim() || '客户'
|
||||
const contractNo = (row.contractNo || '').trim() || '—'
|
||||
const side = stopKindLabel(row.stopKind)
|
||||
const base = `${customer} - ${contractNo}`
|
||||
return side ? `${base}(${side})` : base
|
||||
}
|
||||
|
||||
/** 按合同分组的 HTML 表格(超量 / 剩余 / 框架共用) */
|
||||
export function buildContractQtyDialogHtml(
|
||||
items: Array<Record<string, unknown> | ContractQtyReminderItem>,
|
||||
options?: { intro?: string }
|
||||
): string {
|
||||
const rows = normalizeDialogRows(items)
|
||||
if (!rows.length) {
|
||||
return `<p style="margin:0;color:#64748b;font-size:13px;">暂无明细</p>`
|
||||
}
|
||||
|
||||
const order: string[] = []
|
||||
const groups = new Map<string, ContractQtyDialogRow[]>()
|
||||
for (const row of rows) {
|
||||
const key = groupKey(row)
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, [])
|
||||
order.push(key)
|
||||
}
|
||||
groups.get(key)!.push(row)
|
||||
}
|
||||
|
||||
const blocks = order.map((key) => {
|
||||
const list = groups.get(key) || []
|
||||
const title = groupTitle(list[0])
|
||||
const body = list
|
||||
.map((row) => {
|
||||
const name = escapeHtml(row.productName || row.productId || '—')
|
||||
const model = escapeHtml(row.model || '—')
|
||||
const unit = escapeHtml(row.unit || '—')
|
||||
const contractQty = row.framework
|
||||
? '按客户需求'
|
||||
: escapeHtml(formatQty(row.contractQty))
|
||||
const arranged = escapeHtml(formatQty(row.arrangedTotalQty))
|
||||
const remainingRaw = row.framework ? '—' : formatQty(row.remainingQty)
|
||||
const remainingStyle =
|
||||
row.overflow || (row.remainingQty != null && row.remainingQty < 0)
|
||||
? 'color:#dc2626;font-weight:600;'
|
||||
: 'color:#059669;font-weight:600;'
|
||||
return `<tr>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;">${name}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;">${model}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-variant-numeric:tabular-nums;">${contractQty}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:center;">${unit}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-variant-numeric:tabular-nums;">${arranged}</td>
|
||||
<td style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-variant-numeric:tabular-nums;${remainingStyle}">${escapeHtml(remainingRaw)}</td>
|
||||
</tr>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `<div style="margin:0 0 14px;">
|
||||
<div style="font-size:14px;font-weight:600;color:#1e293b;margin:0 0 8px;padding-left:8px;border-left:3px solid #10b981;">
|
||||
${escapeHtml(title)}
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;color:#1e293b;">
|
||||
<thead>
|
||||
<tr style="background:#f8fafc;">
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;font-weight:600;">产品名称</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:left;font-weight:600;">规格型号</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-weight:600;">合同数量</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:center;font-weight:600;">单位</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-weight:600;">已安排</th>
|
||||
<th style="padding:8px 10px;border:1px solid #d9e8e2;text-align:right;font-weight:600;">剩余</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${body}</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
})
|
||||
|
||||
const intro = options?.intro
|
||||
? `<p style="margin:0 0 12px;font-size:13px;color:#64748b;">${escapeHtml(options.intro)}</p>`
|
||||
: ''
|
||||
return `<div style="max-width:720px;text-align:left;">${intro}${blocks.join('')}</div>`
|
||||
}
|
||||
|
||||
/** @returns true 表示已按合同超量处理 */
|
||||
export async function showContractQtyViolationIfMatch(error: unknown): Promise<boolean> {
|
||||
const parsed = extractContractQtyViolationDetail(error)
|
||||
if (!parsed) return false
|
||||
await ElMessageBox.alert(formatContractQtyViolationMessage(parsed.items), '无法安排运输', {
|
||||
const html = buildContractQtyDialogHtml(parsed.items, {
|
||||
intro: '以下产品「已安排 + 本次」超过合同数量,请调整后再安排:'
|
||||
})
|
||||
await ElMessageBox.alert(html, '无法安排运输', {
|
||||
type: 'error',
|
||||
confirmButtonText: '知道了'
|
||||
confirmButtonText: '知道了',
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'pl-contract-qty-msgbox',
|
||||
customStyle: { width: '780px', maxWidth: '92vw' }
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -95,14 +203,23 @@ export async function showContractQtyReminderIfAny(
|
||||
): Promise<void> {
|
||||
const items = reminder?.items || []
|
||||
if (!items.length) return
|
||||
const message = formatContractQtyReminderMessage(reminder!)
|
||||
if (!message) return
|
||||
const hasFramework = items.some((i) => i.mode === 'framework_cumulative')
|
||||
const title = hasFramework && !items.some((i) => i.mode === 'remaining')
|
||||
? '安排成功 · 框架合同累计'
|
||||
: '安排成功 · 合同剩余数量'
|
||||
await ElMessageBox.alert(message, title, {
|
||||
const hasRemaining = items.some((i) => i.mode === 'remaining')
|
||||
let title = '安排成功 · 合同数量提醒'
|
||||
let intro = '运输安排已成功。各关联合同产品数量如下:'
|
||||
if (hasFramework && !hasRemaining) {
|
||||
title = '安排成功 · 框架合同累计'
|
||||
intro = '运输安排已成功。框架合同无固定数量上限,累计已安排如下:'
|
||||
} else if (hasRemaining && !hasFramework) {
|
||||
title = '安排成功 · 合同剩余数量'
|
||||
intro = '运输安排已成功。各产品剩余可安排量如下:'
|
||||
}
|
||||
const html = buildContractQtyDialogHtml(items, { intro })
|
||||
await ElMessageBox.alert(html, title, {
|
||||
type: 'success',
|
||||
confirmButtonText: '知道了'
|
||||
confirmButtonText: '知道了',
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'pl-contract-qty-msgbox',
|
||||
customStyle: { width: '780px', maxWidth: '92vw' }
|
||||
})
|
||||
}
|
||||
@@ -77,6 +77,87 @@ export function flattenPlanListRows(plans: Plan[]): PlPlanListDisplayRow[] {
|
||||
return rows
|
||||
}
|
||||
|
||||
function stopHasProductId(
|
||||
stop: PlanLoad | PlanDelivery | null | undefined,
|
||||
productId: string
|
||||
): boolean {
|
||||
const pid = String(productId || '').trim()
|
||||
if (!pid || !stop) return false
|
||||
return (stop.products || []).some((p) => String(p.productId || '') === pid)
|
||||
}
|
||||
|
||||
/** 仅保留指定产品(计划产品行 + 各段产品行);不含该产品的送货段丢弃。 */
|
||||
export function filterPlanToProductId(plan: Plan, productId: string): Plan {
|
||||
const pid = String(productId || '').trim()
|
||||
if (!pid) return plan
|
||||
const match = (p: { productId?: string }) => String(p.productId || '') === pid
|
||||
const products = (plan.products || []).filter(match)
|
||||
const load = plan.load
|
||||
? { ...plan.load, products: (plan.load.products || []).filter(match) }
|
||||
: plan.load
|
||||
const deliveries = (plan.deliveries || [])
|
||||
.map((d) => ({ ...d, products: (d.products || []).filter(match) }))
|
||||
.filter((d) => (d.products || []).length > 0)
|
||||
return { ...plan, products, load, deliveries }
|
||||
}
|
||||
|
||||
/**
|
||||
* 展平运输计划列表行,但只保留含指定产品的段次(不展示同计划其他商品)。
|
||||
* 用于合同用量等「单产品」弹窗列表。
|
||||
*/
|
||||
export function flattenPlanListRowsForProduct(
|
||||
plans: Plan[],
|
||||
productId: string
|
||||
): PlPlanListDisplayRow[] {
|
||||
const pid = String(productId || '').trim()
|
||||
if (!pid) return flattenPlanListRows(plans)
|
||||
|
||||
const rows: PlPlanListDisplayRow[] = []
|
||||
let planListIndex = 0
|
||||
for (const raw of plans || []) {
|
||||
if (!raw?.id) continue
|
||||
const plan = filterPlanToProductId(raw, pid)
|
||||
const { load, deliveries } = resolvePlanStopsForList(plan)
|
||||
type StopSlot = {
|
||||
kind: PlPlanListDisplayRowKind
|
||||
stop: PlanLoad | PlanDelivery
|
||||
deliverySeq?: number
|
||||
}
|
||||
const stops: StopSlot[] = []
|
||||
if (stopHasProductId(load, pid)) {
|
||||
stops.push({ kind: 'load', stop: load })
|
||||
}
|
||||
deliveries.forEach((delivery, index) => {
|
||||
if (!stopHasProductId(delivery, pid)) return
|
||||
stops.push({
|
||||
kind: 'delivery',
|
||||
stop: delivery,
|
||||
deliverySeq: delivery.seq ?? index + 1
|
||||
})
|
||||
})
|
||||
if (!stops.length) continue
|
||||
const planRowSpan = stops.length
|
||||
stops.forEach((slot, index) => {
|
||||
const isLoad = slot.kind === 'load'
|
||||
rows.push({
|
||||
rowKey: isLoad
|
||||
? `${plan.id}-load`
|
||||
: `${plan.id}-delivery-${slot.deliverySeq ?? index + 1}`,
|
||||
plan,
|
||||
kind: slot.kind,
|
||||
stop: slot.stop,
|
||||
deliverySeq: slot.deliverySeq,
|
||||
planListIndex,
|
||||
isPlanFirstRow: index === 0,
|
||||
planRowSpan,
|
||||
isPlanLastRow: index === stops.length - 1
|
||||
})
|
||||
})
|
||||
planListIndex += 1
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export function getPlanListDisplayIndex(
|
||||
displayRows: PlPlanListDisplayRow[],
|
||||
displayRowIndex: number,
|
||||
@@ -102,14 +183,16 @@ export function isPlPlanListPlanMergeColumn(
|
||||
}
|
||||
|
||||
export function buildPlPlanListSpanMethod(
|
||||
visiblePlanProps: string[]
|
||||
visiblePlanProps: string[],
|
||||
fixedMergeColCount: number = PLAN_FIXED_MERGE_COL_COUNT
|
||||
): (args: {
|
||||
row: PlPlanListDisplayRow
|
||||
column: { type?: string; property?: string }
|
||||
rowIndex: number
|
||||
columnIndex: number
|
||||
}) => { rowspan: number; colspan: number } | undefined {
|
||||
const planMergeCount = PLAN_FIXED_MERGE_COL_COUNT + visiblePlanProps.length
|
||||
const fixed = Math.max(0, fixedMergeColCount)
|
||||
const planMergeCount = fixed + visiblePlanProps.length
|
||||
return ({ row, columnIndex }) => {
|
||||
if (columnIndex >= planMergeCount) return undefined
|
||||
if (row.isPlanFirstRow) {
|
||||
@@ -120,7 +203,10 @@ export function buildPlPlanListSpanMethod(
|
||||
}
|
||||
|
||||
export function getPlPlanListRowClassName({ row }: { row: PlPlanListDisplayRow }): string {
|
||||
const classes = [row.kind === 'load' ? 'pl-plan-list-row--load' : 'pl-plan-list-row--delivery']
|
||||
const classes = [
|
||||
'pl-plan-list-row',
|
||||
row.kind === 'load' ? 'pl-plan-list-row--load' : 'pl-plan-list-row--delivery'
|
||||
]
|
||||
if (row.planListIndex % 2 === 1) classes.push('pl-plan-list-row--plan-striped')
|
||||
if (row.isPlanLastRow) classes.push('pl-plan-list-row--plan-end')
|
||||
return classes.join(' ')
|
||||
|
||||
@@ -111,7 +111,7 @@ export interface PlanDeliveryForm {
|
||||
products: PlanStopProductFormRow[]
|
||||
}
|
||||
|
||||
/** 地磅默认勾选以本段客户是否为内部客户(山峡/云栋)为准。 */
|
||||
/** 地磅勾选仅对内部客户(山峡/云栋)生效:默认勾选且不可改;其他客户默认不勾且不可勾。 */
|
||||
export function resolveStopDibangDefaults(
|
||||
customerName?: string | null,
|
||||
stopType: 'load' | 'delivery' = 'load'
|
||||
@@ -132,6 +132,10 @@ function normalizeStopDibangFieldsFromApi(
|
||||
dibangDirection?: string | null
|
||||
): { generateDibang: number; dibangDirection: string } {
|
||||
const defaults = resolveStopDibangDefaults(customerName, stopType)
|
||||
// 非山峡/云栋:禁止生成地磅单(编辑回填也强制清掉历史勾选)
|
||||
if (!isPlInternalCustomerName(customerName)) {
|
||||
return { generateDibang: 0, dibangDirection: 'none' }
|
||||
}
|
||||
const gen = Number(generateDibang ?? defaults.generateDibang) === 1 ? 1 : 0
|
||||
let direction = (dibangDirection || '').trim()
|
||||
if (gen) {
|
||||
|
||||
@@ -262,6 +262,42 @@
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="已归档数量"
|
||||
prop="archivedQty"
|
||||
min-width="110"
|
||||
align="right"
|
||||
class-name="col-plan-qty"
|
||||
label-class-name="col-plan-qty"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="plan-qty-num">{{ formatPlanQty(row.archivedQty) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="未归档已安排"
|
||||
prop="arrangedUnarchivedQty"
|
||||
min-width="120"
|
||||
align="right"
|
||||
class-name="col-plan-qty"
|
||||
label-class-name="col-plan-qty"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="plan-qty-num">{{ formatPlanQty(row.arrangedUnarchivedQty) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="已安排合计"
|
||||
prop="arrangedTotalQty"
|
||||
min-width="110"
|
||||
align="right"
|
||||
class-name="col-plan-qty"
|
||||
label-class-name="col-plan-qty"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="plan-qty-num">{{ formatPlanQty(row.arrangedTotalQty) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="包装规格" prop="packageSpec" min-width="150" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.packageSpec || '-' }}</span>
|
||||
@@ -397,6 +433,14 @@
|
||||
<el-empty v-else description="加载中..." />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 运输计划 -->
|
||||
<el-tab-pane label="运输计划" name="plans">
|
||||
<PlContractLinkedPlansPanel
|
||||
:contract-id="detailContract?.id"
|
||||
:active="detailActiveTab === 'plans'"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 回款记录 -->
|
||||
<el-tab-pane label="回款记录" name="payment">
|
||||
<div class="panel dialog-list-panel">
|
||||
@@ -917,6 +961,7 @@ import UploadDialog from './UploadDialog.vue';
|
||||
import ContractFormDialog from './ContractFormDialog.vue';
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue';
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types';
|
||||
import PlContractLinkedPlansPanel from '@/components/business/pl-contract-linked-plans-panel/index.vue';
|
||||
import {
|
||||
parseSettlementClauseDrafts,
|
||||
resolveSettlementClauseContent
|
||||
@@ -3092,6 +3137,12 @@ const handleGeneratePdfFromDialog = async () => {
|
||||
onMounted(async () => {
|
||||
await dictStore.loadAllDict();
|
||||
});
|
||||
|
||||
function formatPlanQty(value: unknown) {
|
||||
const n = parseFloat(String(value ?? 0))
|
||||
if (Number.isNaN(n)) return '0.000'
|
||||
return n.toFixed(3)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.framework-tier-detail-block {
|
||||
@@ -3102,6 +3153,20 @@ onMounted(async () => {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(th.col-plan-qty),
|
||||
:deep(td.col-plan-qty) {
|
||||
color: #059669 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(th.col-plan-qty) {
|
||||
background: #ecfdf5 !important;
|
||||
}
|
||||
.plan-qty-num {
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.tier-letter-thumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -336,6 +336,42 @@
|
||||
<span v-else>¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="已归档数量"
|
||||
prop="archivedQty"
|
||||
min-width="110"
|
||||
align="right"
|
||||
class-name="col-plan-qty"
|
||||
label-class-name="col-plan-qty"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="plan-qty-num">{{ formatPlanQty(row.archivedQty) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="未归档已安排"
|
||||
prop="arrangedUnarchivedQty"
|
||||
min-width="120"
|
||||
align="right"
|
||||
class-name="col-plan-qty"
|
||||
label-class-name="col-plan-qty"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="plan-qty-num">{{ formatPlanQty(row.arrangedUnarchivedQty) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="已安排合计"
|
||||
prop="arrangedTotalQty"
|
||||
min-width="110"
|
||||
align="right"
|
||||
class-name="col-plan-qty"
|
||||
label-class-name="col-plan-qty"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="plan-qty-num">{{ formatPlanQty(row.arrangedTotalQty) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="包装规格" prop="packageSpec" min-width="150" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.packageSpec || '-' }}</span>
|
||||
@@ -462,6 +498,14 @@
|
||||
<el-empty v-else description="加载中..." />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 运输计划 -->
|
||||
<el-tab-pane label="运输计划" name="plans">
|
||||
<PlContractLinkedPlansPanel
|
||||
:contract-id="contract?.id"
|
||||
:active="activeTab === 'plans'"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 回款记录 -->
|
||||
<el-tab-pane label="回款记录" name="payment">
|
||||
<el-button type="primary" icon="Plus" @click="handleAddPayment" class="mb-3">新增回款</el-button>
|
||||
@@ -1092,6 +1136,7 @@ import ExecuteDialog from './components/ExecuteDialog.vue';
|
||||
import InvalidDialog from './components/InvalidDialog.vue';
|
||||
import RenewDialog from './components/RenewDialog.vue';
|
||||
import UploadDialog from './components/UploadDialog.vue';
|
||||
import PlContractLinkedPlansPanel from '@/components/business/pl-contract-linked-plans-panel/index.vue';
|
||||
import CustomerDetailDialog from '@/views/crm/customer/CustomerDetailDialog.vue';
|
||||
import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||
import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
|
||||
@@ -2902,10 +2947,30 @@ const handleGeneratePdfFile = async () => {
|
||||
pdfGenerating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function formatPlanQty(value: unknown) {
|
||||
const n = parseFloat(String(value ?? 0))
|
||||
if (Number.isNaN(n)) return '0.000'
|
||||
return n.toFixed(3)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contract-detail-page {
|
||||
:deep(th.col-plan-qty),
|
||||
:deep(td.col-plan-qty) {
|
||||
color: #059669 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(th.col-plan-qty) {
|
||||
background: #ecfdf5 !important;
|
||||
}
|
||||
.plan-qty-num {
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.detail-subtitle {
|
||||
margin: 0;
|
||||
padding: 0 16px 12px;
|
||||
|
||||
@@ -298,16 +298,8 @@
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
v-if="
|
||||
row.plan.canCopyToDriver ||
|
||||
row.plan.canCopyToSite ||
|
||||
row.plan.canCopyPoundQrcode
|
||||
"
|
||||
class="pl-plan-arrange-copy-cell"
|
||||
>
|
||||
<div v-if="row.plan.canCopyPoundQrcode" class="pl-plan-arrange-copy-cell">
|
||||
<el-button
|
||||
v-if="row.plan.canCopyPoundQrcode"
|
||||
class="btn-ghost"
|
||||
title="复制磅单二维码"
|
||||
:loading="
|
||||
@@ -317,28 +309,6 @@
|
||||
>
|
||||
磅单码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.plan.canCopyToDriver"
|
||||
class="btn-ghost"
|
||||
title="复制给司机"
|
||||
:loading="
|
||||
arrangeCopyLoadingId === row.plan.id && arrangeCopyLoadingAction === 'driver'
|
||||
"
|
||||
@click.stop="handleListCopyDriver(row.plan)"
|
||||
>
|
||||
司机
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.plan.canCopyToSite"
|
||||
class="btn-ghost"
|
||||
title="复制给现场"
|
||||
:loading="
|
||||
arrangeCopyLoadingId === row.plan.id && arrangeCopyLoadingAction === 'site'
|
||||
"
|
||||
@click.stop="handleListCopySite(row.plan)"
|
||||
>
|
||||
现场
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
@@ -932,31 +902,21 @@
|
||||
poundQrcodePickVisible,
|
||||
poundQrcodePickRecords,
|
||||
loadArrangeActions,
|
||||
copySiteSummary,
|
||||
copyPlanSummary,
|
||||
copyPoundSlipQrcode,
|
||||
handlePoundQrcodePickCopy,
|
||||
handlePoundQrcodePickPreview
|
||||
} = usePlPlanArrangeActions()
|
||||
|
||||
const arrangeCopyLoadingId = ref('')
|
||||
const arrangeCopyLoadingAction = ref<'driver' | 'site' | 'qrcode' | ''>('')
|
||||
const arrangeCopyLoadingAction = ref<'qrcode' | ''>('')
|
||||
|
||||
type ArrangeCopyAction = 'driver' | 'site' | 'qrcode'
|
||||
|
||||
async function runListArrangeCopy(row: Plan, action: ArrangeCopyAction) {
|
||||
async function handleListCopyPoundQrcode(row: Plan) {
|
||||
if (!row.id || arrangeCopyLoadingId.value) return
|
||||
arrangeCopyLoadingId.value = row.id
|
||||
arrangeCopyLoadingAction.value = action
|
||||
arrangeCopyLoadingAction.value = 'qrcode'
|
||||
try {
|
||||
await loadArrangeActions(row.id)
|
||||
if (action === 'driver') {
|
||||
await copyPlanSummary()
|
||||
} else if (action === 'site') {
|
||||
await copySiteSummary()
|
||||
} else {
|
||||
await copyPoundSlipQrcode()
|
||||
}
|
||||
await copyPoundSlipQrcode()
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请稍后重试')
|
||||
} finally {
|
||||
@@ -965,18 +925,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleListCopyDriver(row: Plan) {
|
||||
void runListArrangeCopy(row, 'driver')
|
||||
}
|
||||
|
||||
function handleListCopySite(row: Plan) {
|
||||
void runListArrangeCopy(row, 'site')
|
||||
}
|
||||
|
||||
function handleListCopyPoundQrcode(row: Plan) {
|
||||
void runListArrangeCopy(row, 'qrcode')
|
||||
}
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
|
||||
const planStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
@@ -1756,8 +1704,10 @@
|
||||
background-color: #ecfdf5 !important;
|
||||
}
|
||||
|
||||
tbody tr.pl-plan-list-row:hover > td.el-table__cell {
|
||||
background-color: var(--el-table-row-hover-bg-color, #f1f5f9) !important;
|
||||
/* 悬停用浅红;须压过斑马纹/装货段 !important 底色 */
|
||||
tbody tr.pl-plan-list-row:hover > td.el-table__cell,
|
||||
tbody tr.pl-plan-list-row.hover-row > td.el-table__cell {
|
||||
background-color: #fee2e2 !important;
|
||||
}
|
||||
|
||||
.pl-plan-list-row--plan-end > td {
|
||||
|
||||
@@ -194,6 +194,10 @@
|
||||
import { useMenuFavoriteStore } from '@/store/modules/menuFavorite'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import {
|
||||
getPortalHomeActiveModule,
|
||||
setPortalHomeActiveModule
|
||||
} from '@/utils/navigation/portal-home-active-module'
|
||||
import {
|
||||
collectPortalHomeSections,
|
||||
findHomeCardByPath,
|
||||
@@ -232,7 +236,7 @@
|
||||
const { items: favItems } = storeToRefs(favStore)
|
||||
const { counts: badgeCounts, loaded: badgeLoaded } = storeToRefs(badgeStore)
|
||||
|
||||
const activeModule = ref(FAV_KEY)
|
||||
const activeModule = ref(getPortalHomeActiveModule() || FAV_KEY)
|
||||
const favoriteList = ref<PortalHomeCardItem[]>([])
|
||||
const favDragging = ref(false)
|
||||
const suppressFavClick = ref(false)
|
||||
@@ -393,6 +397,7 @@
|
||||
}
|
||||
|
||||
watch(modules, ensureActiveModule, { immediate: true, deep: true })
|
||||
watch(activeModule, (key) => setPortalHomeActiveModule(key))
|
||||
watch([favItems, modules], syncFavoriteList, { immediate: true, deep: true })
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
Reference in new issue
Block a user