This commit is contained in:
zlwu committed 2026-08-03 21:20:00 +08:00
1 parent e233269abf
commit 732c1b1dc9
17 files changed
+9607 -88132

No files matched your search

@@ -14,115 +14,121 @@
@closed="handleClosed"
@opened="handleOpened"
>
<div class="pl-arrange-layout">
<section class="pl-arrange-form-panel">
<div v-loading="loading" class="pl-arrange-form-scroll">
<div ref="feedbackMirrorEl" class="pl-archive-feedback-mirror" />
<div v-loading="loading" class="pl-archive-layout">
<el-tabs v-model="activeTab" class="pl-archive-tabs">
<el-tab-pane label="归档填写" name="archive" />
<el-tab-pane label="计划概况" name="overview" />
<el-tab-pane label="运输信息" name="transport" />
<el-tab-pane label="合同信息" name="contract" />
</el-tabs>
<section class="pl-plan-form-section">
<h4 class="pl-plan-subtitle">产品实际数量</h4>
<PlPlanFormProductTable
:products="productRows"
show-actual-qty
editable-actual-qty-only
<div v-show="activeTab === 'archive'" class="pl-archive-tab-panel pl-arrange-form-scroll">
<div ref="feedbackMirrorEl" class="pl-archive-feedback-mirror" />
<section class="pl-plan-form-section">
<h3 class="pl-plan-form-section-title">产品实际数量</h3>
<PlPlanFormProductTable
:products="productRows"
show-actual-qty
editable-actual-qty-only
/>
<div
v-for="row in brandedProductRows"
:key="row.id || row.productId"
class="pl-archive-brand-qty-block"
>
<div class="pl-archive-brand-qty-head">
<span class="pl-archive-brand-qty-title">{{ row.productName }} · 按品牌填量</span>
<el-tag v-if="row.brandMode === 'required'" size="small" type="warning"
>指定品牌</el-tag
>
<el-tag v-else size="small" type="info">不限品牌</el-tag>
</div>
<BrandQtyChipsField
v-model="row.brandQtyRows"
:brand-mode="archiveBrandMode(row)"
:required-brand-id="row.brandId"
:brand-options="row.brandOptions || []"
:unit="resolveProductUnitLabel(row.unit)"
variant="admin"
:actual-qty="Number(row.actualQty ?? 0)"
@update:actual-qty="(val) => onArchiveBrandActualQty(row, val)"
/>
<div
v-for="row in brandedProductRows"
:key="row.id || row.productId"
class="pl-archive-brand-qty-block"
>
<div class="pl-archive-brand-qty-head">
<span class="pl-archive-brand-qty-title">{{ row.productName }} · 按品牌填量</span>
<el-tag v-if="row.brandMode === 'required'" size="small" type="warning"
>指定品牌</el-tag
>
<el-tag v-else size="small" type="info">不限品牌</el-tag>
</div>
<BrandQtyChipsField
v-model="row.brandQtyRows"
:brand-mode="archiveBrandMode(row)"
:required-brand-id="row.brandId"
:brand-options="row.brandOptions || []"
:unit="resolveProductUnitLabel(row.unit)"
variant="admin"
:actual-qty="Number(row.actualQty ?? 0)"
@update:actual-qty="(val) => onArchiveBrandActualQty(row, val)"
/>
</div>
</section>
</div>
</section>
<section class="pl-plan-form-section pl-plan-freight-section">
<div class="pl-plan-freight-section-head">
<h4 class="pl-plan-subtitle">运费信息</h4>
<label class="pl-plan-freight-switch">
<span class="pl-plan-freight-switch-label">是否有运输费用</span>
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
</label>
<section class="pl-plan-form-section pl-plan-freight-section">
<div class="pl-plan-freight-section-head">
<h3 class="pl-plan-form-section-title">运费信息</h3>
<label class="pl-plan-freight-switch">
<span class="pl-plan-freight-switch-label">是否有运输费用</span>
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
</label>
</div>
<el-form
v-if="formData.hasFreight === 1"
:model="formData"
label-position="top"
class="pl-plan-freight-form"
>
<div class="form-grid-5 pl-plan-archive-freight-grid">
<el-form-item label="运费计算方式">
<el-select v-model="formData.freightType">
<el-option label="按重量" value="by_weight" />
<el-option label="按趟" value="by_trip" />
</el-select>
</el-form-item>
<el-form-item label="运输公里数">
<el-input-number v-model="formData.distanceKm" :min="0" :precision="2" />
</el-form-item>
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输量(${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.transportQty.value"
:min="0"
:precision="3"
@change="freightCalc.markTransportQtyTouched()"
/>
</el-form-item>
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输单价(元/${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.freightPrice.value"
:min="0"
:precision="2"
/>
</el-form-item>
<el-form-item label="运费金额(元)">
<el-input-number
v-model="freightCalc.freightAmount.value"
:min="0"
:precision="2"
@change="freightCalc.markFreightAmountTouched()"
/>
</el-form-item>
</div>
<el-form
v-if="formData.hasFreight === 1"
:model="formData"
label-position="top"
class="pl-plan-freight-form"
>
<div class="form-grid-5 pl-plan-archive-freight-grid">
<el-form-item label="运费计算方式">
<el-select v-model="formData.freightType">
<el-option label="按重量" value="by_weight" />
<el-option label="按趟" value="by_trip" />
</el-select>
</el-form-item>
<el-form-item label="运输公里数">
<el-input-number v-model="formData.distanceKm" :min="0" :precision="2" />
</el-form-item>
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输量(${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.transportQty.value"
:min="0"
:precision="3"
@change="freightCalc.markTransportQtyTouched()"
/>
</el-form-item>
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输单价(元/${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.freightPrice.value"
:min="0"
:precision="2"
/>
</el-form-item>
<el-form-item label="运费金额(元)">
<el-input-number
v-model="freightCalc.freightAmount.value"
:min="0"
:precision="2"
@change="freightCalc.markFreightAmountTouched()"
/>
</el-form-item>
</div>
</el-form>
</section>
</div>
</section>
</el-form>
</section>
</div>
<section class="pl-arrange-view-panel">
<h3 class="pl-arrange-view-title">计划单查看</h3>
<div v-show="activeTab !== 'archive'" class="pl-archive-tab-panel pl-arrange-view-panel">
<PlPlanViewTabs
v-if="planId && feedbackMirrorTarget"
ref="tabsRef"
:plan-id="planId"
embedded
hide-nav
:tab="viewTab"
feedback-editable
:arrange-driver-url="driverUrl"
:arrange-site-records="siteRecords"
:feedback-mirror-to="feedbackMirrorTarget"
:feedback-mirror-to="archiveFeedbackMirrorTo"
/>
</section>
</div>
</div>
<template #footer>
@@ -183,11 +189,19 @@
const loading = ref(false)
const submitLoading = ref(false)
const activeTab = ref<'archive' | 'overview' | 'transport' | 'contract'>('archive')
const tabsRef = ref<InstanceType<typeof PlPlanViewTabs> | null>(null)
const plan = ref<Partial<Plan>>({})
const feedbackMirrorEl = ref<HTMLElement | null>(null)
/** 仅在弹窗 opened 且镜像节点已挂载后传入,避免 Teleport 目标为空导致 Vue patch 崩溃 */
const feedbackMirrorTarget = ref<HTMLElement | null>(null)
/** 归档填写 Tab 时把回传区镜像过去;其他 Tab 时留在运输信息区内 */
const archiveFeedbackMirrorTo = computed(() =>
activeTab.value === 'archive' ? feedbackMirrorTarget.value : null
)
const viewTab = computed(() =>
activeTab.value === 'archive' ? 'overview' : activeTab.value
)
const { driverUrl, siteRecords, resetArrangeActions, loadArrangeActions } =
usePlPlanArrangeActions()
@@ -378,6 +392,7 @@
}
function resetState() {
activeTab.value = 'archive'
plan.value = {}
productRows.value = []
Object.assign(formData, {
@@ -578,16 +593,23 @@
</script>
<style scoped lang="scss">
.pl-arrange-layout {
display: flex;
flex-direction: column;
gap: 16px;
.pl-archive-layout {
min-height: 480px;
}
.pl-arrange-form-panel {
flex: 0 0 auto;
overflow: visible;
.pl-archive-tabs {
:deep(> .el-tabs__header) {
margin-bottom: 16px;
}
/* 仅用顶部导航,内容区由下方面板接管 */
:deep(> .el-tabs__content) {
display: none;
}
}
.pl-archive-tab-panel {
min-height: 360px;
}
.pl-arrange-form-scroll {
@@ -608,30 +630,28 @@
}
.pl-arrange-view-panel {
flex: 0 0 auto;
min-height: 280px;
display: flex;
flex-direction: column;
overflow: visible;
}
.pl-arrange-view-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
color: var(--section-bar-title-color, var(--primary-dark, #059669));
}
.pl-plan-form-section {
flex: 0 0 auto;
min-width: 0;
}
.pl-plan-subtitle {
margin: 0 0 10px;
font-size: 13px;
.pl-plan-form-section-title {
margin: 0 0 12px;
padding-left: 10px;
font-size: 14px;
font-weight: 600;
color: #475569;
color: var(--section-bar-title-color, var(--primary-dark, #059669));
border-left: 3px solid var(--section-bar-border-color, var(--primary, #10b981));
line-height: 1;
}
.pl-plan-freight-section-head .pl-plan-form-section-title {
margin-bottom: 0;
}
.pl-archive-brand-qty-block {
@@ -657,11 +677,6 @@
}
.pl-plan-freight-section {
.pl-plan-subtitle {
margin-top: 4px;
margin-bottom: 0;
}
:deep(.el-form-item__label) {
width: 140px !important;
max-width: 140px;
@@ -31,21 +31,28 @@
<el-tag v-if="item.isExplosive" type="danger" size="small">易制爆</el-tag>
<el-tag v-if="item.isToxic" type="warning" size="small">易制毒</el-tag>
</div>
<div class="form-grid-2 product-cert-grid">
<el-form-item
v-for="slot in getRequiredCertSlots(item)"
:key="slot.key"
:label="slot.label"
:class="{ 'product-cert-grid__span-all': getRequiredCertSlots(item).length === 1 }"
>
<AttachmentUploadField
v-model="item[slot.key]"
folder="pl/plan-cert"
:limit="1"
:hint="`支持图片、PDF 等,上传${slot.label}`"
/>
</el-form-item>
</div>
<el-form
label-position="left"
label-width="72px"
class="product-cert-form"
@submit.prevent
>
<div class="form-grid-2 product-cert-grid">
<el-form-item
v-for="slot in getRequiredCertSlots(item)"
:key="slot.key"
:label="slot.label"
:class="{ 'product-cert-grid__span-all': getRequiredCertSlots(item).length === 1 }"
>
<AttachmentUploadField
v-model="item[slot.key]"
folder="pl/plan-cert"
:limit="1"
:hint="`支持图片、PDF 等,上传${slot.label}`"
/>
</el-form-item>
</div>
</el-form>
</div>
</div>
@@ -200,10 +207,27 @@ function handleClosed() {
color: var(--text, #1e293b);
}
.product-cert-grid {
.product-cert-form {
:deep(.el-form-item) {
align-items: flex-start;
margin-bottom: 0;
}
/* 购买证 / 运输证 / 合法证明 统一标题宽度,四字不换行,附件上传区左对齐 */
:deep(.el-form-item__label) {
flex-shrink: 0;
width: 72px !important;
justify-content: flex-start;
text-align: left;
white-space: nowrap;
line-height: 1.4 !important;
height: auto !important;
padding-top: 8px;
}
:deep(.el-form-item__content) {
min-width: 0;
}
}
.product-cert-grid__span-all {
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,10 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div v-loading="loading" class="pl-plan-view-tabs" :class="{ 'is-embedded': embedded }">
<div
v-loading="loading"
class="pl-plan-view-tabs"
:class="{ 'is-embedded': embedded, 'is-hide-nav': hideNav }"
>
<el-tabs v-model="activeTab" class="pl-plan-view-tabs__nav">
<el-tab-pane label="计划概况" name="overview">
<section class="pl-plan-form-section pl-plan-form-section--plain">
@@ -80,8 +84,8 @@
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">物流节点</h3>
<div class="parties-row">
<div class="party-panel party-panel--plain">
<ViewKvTable :fields="pickupViewFields" mode="quad" />
<div class="party-panel party-panel--plain pl-plan-logistics-panel">
<ViewKvTable :fields="pickupViewFields" mode="dual" />
</div>
<div class="party-flow-bridge">
@@ -105,8 +109,8 @@
</span>
</div>
<div class="party-panel party-panel--plain">
<ViewKvTable :fields="deliveryViewFields" mode="quad" />
<div class="party-panel party-panel--plain pl-plan-logistics-panel">
<ViewKvTable :fields="deliveryViewFields" mode="dual" />
</div>
</div>
</section>
@@ -347,7 +351,6 @@
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { businessTypeTagType } from '@/components/business/db/db-related-view'
import type { ContractVO } from '@/api/crm/contract/types'
import { inventoryApi } from '@/api/db/inventory'
import {
@@ -362,7 +365,7 @@
mapContractProducts,
type PlanFormProductRow
} from '@/composables/usePlContractToPlan'
import { mapPlanProductToFormRow, usePlPlanViewDetail, type PlPlanTransportDibangRow, type PlPlanTransportSiteRow } from '@/composables/usePlPlanViewDetail'
import { mapPlanProductToFormRow, usePlPlanViewDetail, type PlPlanTransportSiteRow } from '@/composables/usePlPlanViewDetail'
import { resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
import { useDictStore } from '@/store/modules/dict'
@@ -400,6 +403,10 @@
feedbackEditable?: boolean
/** 将司机/现场/地磅回传区块镜像渲染到归档左侧等目标节点 */
feedbackMirrorTo?: string | HTMLElement | null
/** 隐藏内部 Tab 导航(由外层统一控制 Tab) */
hideNav?: boolean
/** 受控当前 Tab:overview / transport / contract */
tab?: string
}>(),
{
embedded: false,
@@ -407,11 +414,33 @@
arrangeDriverUrl: '',
arrangeSiteRecords: () => [],
feedbackEditable: false,
feedbackMirrorTo: null
feedbackMirrorTo: null,
hideNav: false,
tab: undefined
}
)
const activeTab = ref('overview')
const emit = defineEmits<{
'update:tab': [value: string]
}>()
const innerActiveTab = ref(props.tab || 'overview')
const activeTab = computed({
get: () => props.tab || innerActiveTab.value,
set: (value: string) => {
innerActiveTab.value = value
emit('update:tab', value)
}
})
watch(
() => props.tab,
(value) => {
if (value && value !== innerActiveTab.value) {
innerActiveTab.value = value
}
}
)
const contractDetailDialogActive = ref(false)
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
const vehicleViewVisible = ref(false)
@@ -899,28 +928,6 @@
}))
)
function dibangBasicViewFields(row: PlPlanTransportDibangRow): ViewField[] {
const businessType = row.inventory?.businessType
// long: false — 强制六字段同一行展示,避免往来单位等长文案自动折行
return [
{ label: '出入库单号', value: row.inventory?.batchNo || row.link.batchNo, long: false },
{
label: '业务类型',
value: businessType,
tagType: businessType ? businessTypeTagType(businessType) : undefined,
long: false
},
{ label: '往来单位', value: row.inventory?.partnerCompany, long: false },
{
label: '车牌号',
value: row.inventory?.vehicleNo || plan.value?.licensePlate || transport.value?.licensePlate,
long: false
},
{ label: '产品', value: row.inventory?.product || row.link.productName, long: false },
{ label: '数量(吨)', value: row.inventory?.quantity, long: false }
]
}
const dibangPlanRows = computed(() =>
dibangRows.value.filter(row => Boolean(row.link.dbPurchasePlanId?.trim()))
)
@@ -1048,8 +1055,7 @@
ioOrderViewFields: ioOrderViewFields.value,
ioOrderImageCsv: ioOrderImageCsv.value,
siteFeedbackTableRows: siteFeedbackTableRows.value,
dibangPlanRows: dibangPlanRows.value,
dibangBasicViewFields
dibangPlanRows: dibangPlanRows.value
}))
const feedbackBlocksOn = {
@@ -1284,6 +1290,16 @@
margin-top: 0;
}
}
&.is-hide-nav {
.pl-plan-view-tabs__nav :deep(> .el-tabs__header) {
display: none;
}
.pl-plan-view-tabs__nav :deep(> .el-tabs__content) {
padding-top: 0;
}
}
}
.pl-plan-view-tabs__nav :deep(.el-tabs__content) {
@@ -1449,6 +1465,23 @@
}
}
.pl-plan-logistics-panel {
:deep(.view-kv-table--dual th) {
width: 88px;
white-space: nowrap;
}
:deep(.view-kv-table--dual td) {
width: auto;
min-width: 7.5em;
}
/* 取货/送达时间所在值列禁止换行,避免 2026-07-31 被拆开 */
:deep(.view-kv-table--dual tr:not(.row-long) td:first-of-type) {
white-space: nowrap;
}
}
.party-panel__title {
margin-bottom: 12px;
padding-left: 10px;
@@ -15,9 +15,13 @@
<li v-for="(item, idx) in field.items" :key="idx">{{ item }}</li>
</ul>
<template v-else>
<span :class="{ 'money-display': field.money }">{{
formatViewValue(field.value)
}}</span>
<span
:class="{
'money-display': field.money,
'danger-display': field.danger
}"
>{{ formatViewValue(field.value) }}</span
>
</template>
</dd>
</div>
@@ -60,6 +64,22 @@
&--cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
&--cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
&--cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
&--cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
&--cols-9 {
grid-template-columns: repeat(9, minmax(0, 1fr));
}
}
.view-kv {
@@ -85,6 +105,11 @@
word-break: break-word;
line-height: 1.5;
}
.danger-display {
color: #dc2626;
font-weight: 600;
}
}
.view-kv-span-2 {
@@ -105,9 +130,20 @@
}
}
@media (max-width: 1400px) {
.view-kv-grid--cols-8,
.view-kv-grid--cols-9 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.view-kv-grid--cols-4,
.view-kv-grid--cols-5 {
.view-kv-grid--cols-5,
.view-kv-grid--cols-6,
.view-kv-grid--cols-7,
.view-kv-grid--cols-8,
.view-kv-grid--cols-9 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@@ -115,7 +151,11 @@
@media (max-width: 900px) {
.view-kv-grid--cols-3,
.view-kv-grid--cols-4,
.view-kv-grid--cols-5 {
.view-kv-grid--cols-5,
.view-kv-grid--cols-6,
.view-kv-grid--cols-7,
.view-kv-grid--cols-8,
.view-kv-grid--cols-9 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@@ -124,7 +164,11 @@
.view-kv-grid,
.view-kv-grid--cols-3,
.view-kv-grid--cols-4,
.view-kv-grid--cols-5 {
.view-kv-grid--cols-5,
.view-kv-grid--cols-6,
.view-kv-grid--cols-7,
.view-kv-grid--cols-8,
.view-kv-grid--cols-9 {
grid-template-columns: 1fr;
}