fix(pl): 计划详情用关联证件附件替换危化品证书区。

Web 与 H5 详情将关联购买证/运输证移到装送货后并以附件卡片展示;用证接口回填证件附件 URL。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-15 14:46:46 +08:00
1 parent ea46761f40
commit 95ecb7236e
5 files changed
+310 -39

No files matched your search

@@ -71,23 +71,6 @@
<p v-else-if="!loading" class="pl-plan-form-empty-hint">暂无车辆信息</p>
</section>
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">关联购买证 / 运输证</h3>
<p v-if="plan?.quotaCertPendingFulfill" class="pl-plan-form-empty-hint">
证书齐备(待完善):请到「证书齐备 → 已标记待完善证书」挂证并完善并关闭
</p>
<div v-else-if="linkedCertRows.length" class="pl-plan-linked-certs">
<div v-for="item in linkedCertRows" :key="item.key" class="pl-plan-linked-cert-row">
<el-tag size="small" :type="item.kind === 'purchase' ? 'success' : 'warning'">
{{ item.kind === 'purchase' ? '购买证' : '运输证' }}
</el-tag>
<span>{{ item.certNo || item.certId }}</span>
<span class="text-slate-500">预占 {{ item.reservedQty }} / 已用 {{ item.usedQty }}</span>
</div>
</div>
<p v-else class="pl-plan-form-empty-hint">暂无关联证件</p>
</section>
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">装送货信息</h3>
<PlPlanStopsSplitShell
@@ -251,8 +234,31 @@
</section>
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">危化品证书信息</h3>
<PlPlanHazardCertViewTable :products="hazardProducts" />
<h3 class="pl-plan-form-section-title">关联购买证 / 运输证</h3>
<p v-if="plan?.quotaCertPendingFulfill" class="pl-plan-form-empty-hint">
证书齐备(待完善):请到「证书齐备 → 已标记待完善证书」挂证并完善并关闭
</p>
<div v-else-if="linkedCertRows.length" class="pl-plan-linked-certs">
<div v-for="item in linkedCertRows" :key="item.key" class="pl-plan-linked-cert-block">
<div class="pl-plan-linked-cert-block__meta">
<el-tag size="small" :type="item.kind === 'purchase' ? 'success' : 'warning'">
{{ item.kind === 'purchase' ? '购买证' : '运输证' }}
</el-tag>
<span class="pl-plan-linked-cert-block__no">{{ item.certNo || item.certId }}</span>
<span class="pl-plan-linked-cert-block__qty"
>预占 {{ item.reservedQty }} / 已用 {{ item.usedQty }}</span
>
</div>
<div class="pl-plan-linked-cert-block__slot">
<span class="pl-plan-linked-cert-block__label">证件附件</span>
<div v-if="item.attachmentUrls" class="pl-plan-linked-cert-block__attachment">
<AttachmentUploadField :model-value="item.attachmentUrls" disabled />
</div>
<span v-else class="pl-plan-linked-cert-block__missing">未上传</span>
</div>
</div>
</div>
<p v-else class="pl-plan-form-empty-hint">暂无关联证件</p>
</section>
<section class="pl-plan-form-section pl-plan-form-section--plain">
@@ -430,7 +436,6 @@
type BrandQtyRow
} 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'
@@ -476,7 +481,6 @@
} 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'
import { formatDate, formatDateTime } from '@/utils/format'
import {
@@ -1638,18 +1642,6 @@
return fields
})
/** 危化证区域展示 CRM 标准名(productRows 已回填 standardProductName;库内 productName 可能是别名) */
const hazardProducts = computed(() =>
products.value
.filter((item) => isHazardProduct(item))
.map((item) => {
const formRow = item.id ? planProductFormById.value.get(String(item.id)) : undefined
const standardName = String(formRow?.standardProductName || '').trim()
if (!standardName || standardName === item.productName) return item
return { ...item, productName: standardName }
})
)
const linkedCertRows = ref<
Array<{
key: string
@@ -1658,6 +1650,7 @@
certNo?: string
reservedQty: string | number
usedQty: string | number
attachmentUrls: string
}>
>([])
@@ -1670,9 +1663,10 @@
key: `${u.certType || u.cert_type}-${u.certId || u.cert_id}-${index}`,
kind: String(u.certType || u.cert_type || ''),
certId: String(u.certId || u.cert_id || ''),
certNo: u.certNo || u.cert_no,
certNo: u.certNo || u.cert_no || u.certNoSnapshot || u.cert_no_snapshot,
reservedQty: u.reservedQty ?? u.reserved_qty ?? '—',
usedQty: u.usedQty ?? u.used_qty ?? '—'
usedQty: u.usedQty ?? u.used_qty ?? '—',
attachmentUrls: String(u.attachmentUrls || u.attachment_urls || '').trim()
}))
} catch {
linkedCertRows.value = []
@@ -1932,6 +1926,93 @@
white-space: pre-wrap;
}
.pl-plan-linked-certs {
display: flex;
flex-direction: column;
gap: 12px;
}
.pl-plan-linked-cert-block {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 10px 16px;
padding: 14px 16px;
border: 1px solid var(--border, #d9e8e2);
border-radius: 10px;
background: #fff;
}
.pl-plan-linked-cert-block__meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 12px;
min-width: 0;
}
.pl-plan-linked-cert-block__no {
font-size: 14px;
font-weight: 600;
color: #1e293b;
word-break: break-all;
}
.pl-plan-linked-cert-block__qty {
font-size: 13px;
color: var(--mint-text-muted, #64748b);
}
.pl-plan-linked-cert-block__slot {
display: inline-flex;
align-items: flex-start;
gap: 8px;
}
.pl-plan-linked-cert-block__label {
flex-shrink: 0;
color: var(--mint-text);
font-size: 13px;
line-height: 1.4;
white-space: nowrap;
}
.pl-plan-linked-cert-block__attachment {
display: inline-flex;
flex-shrink: 0;
:deep(.attachment-upload) {
width: auto;
}
:deep(.attachment-list) {
gap: 8px;
}
:deep(.attachment-item) {
width: 140px;
margin-bottom: 0;
border-radius: 8px;
}
:deep(.attachment-preview) {
height: 96px;
}
}
.pl-plan-linked-cert-block__missing {
display: inline-flex;
align-items: center;
justify-content: center;
width: 140px;
height: 96px;
border: 1px dashed var(--border, #d9e8e2);
border-radius: 8px;
background: var(--table-head, #f8fafc);
color: var(--mint-text-muted);
font-size: 12px;
}
.pl-plan-kv-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
+117 -2
View File
@@ -248,7 +248,37 @@
</section>
<section id="plan-sec-cert" class="mp-wv-section mp-h5-anchor">
<h3>危化品证书信息</h3>
<h3>关联购买证 / 运输证</h3>
<p v-if="plan.quotaCertPendingFulfill" class="plan-empty-hint">
证书齐备(待完善):请到「证书齐备 → 已标记待完善证书」挂证并完善并关闭
</p>
<div v-else-if="linkedCertRows.length" class="plan-linked-certs">
<div v-for="item in linkedCertRows" :key="item.key" class="plan-linked-cert-block">
<div class="plan-linked-cert-block__meta">
<span
class="status-tag"
:class="
item.kind === 'purchase' ? 'status-tag--success' : 'status-tag--warning'
"
>{{ item.kind === 'purchase' ? '购买证' : '运输证' }}</span
>
<span class="plan-linked-cert-block__no">{{ item.certNo || item.certId }}</span>
<span class="plan-vehicle-card__meta"
>预占 {{ item.reservedQty }} / 已用 {{ item.usedQty }}</span
>
</div>
<div class="plan-linked-cert-block__slot">
<div class="plan-subcard__label">证件附件</div>
<AttachmentUploadField
v-if="item.attachmentUrls"
:model-value="item.attachmentUrls"
disabled
/>
<p v-else class="plan-linked-cert-block__missing">未上传</p>
</div>
</div>
</div>
<p v-else class="plan-empty-hint">暂无关联证件</p>
<div class="plan-cert-actions">
<van-button block plain type="primary" @click="quotaOpen = true">查看用证</van-button>
<van-button
@@ -536,10 +566,40 @@
const errorText = ref('')
const plan = ref<Plan | null>(null)
const quotaOpen = ref(false)
const linkedCertRows = ref<
Array<{
key: string
kind: string
certId: string
certNo?: string
reservedQty: string | number
usedQty: string | number
attachmentUrls: string
}>
>([])
const needsLegalUseDocument = computed(() =>
(plan.value?.products || []).some((row) => resolveProductHazardFlags(row).isExplosive)
)
const quotaTitle = computed(() => `计划用证 · ${plan.value?.planNo || ''}`)
async function loadLinkedCerts(planId: string) {
linkedCertRows.value = []
try {
const { planQuotaApi } = await import('@/api/pl/quota-cert')
const data = await planQuotaApi.get(planId)
linkedCertRows.value = (data?.usages || []).map((u: any, index: number) => ({
key: `${u.certType || u.cert_type}-${u.certId || u.cert_id}-${index}`,
kind: String(u.certType || u.cert_type || ''),
certId: String(u.certId || u.cert_id || ''),
certNo: u.certNo || u.cert_no || u.certNoSnapshot || u.cert_no_snapshot,
reservedQty: u.reservedQty ?? u.reserved_qty ?? '—',
usedQty: u.usedQty ?? u.used_qty ?? '—',
attachmentUrls: String(u.attachmentUrls || u.attachment_urls || '').trim()
}))
} catch {
linkedCertRows.value = []
}
}
const deliveryIndex = ref(0)
const moreOpen = ref(false)
const voidOpen = ref(false)
@@ -568,7 +628,7 @@
{ key: 'base', label: '基础信息', anchor: 'plan-sec-base' },
{ key: 'transport', label: '运输信息', anchor: 'plan-sec-transport' },
{ key: 'load', label: '装送货', anchor: 'plan-sec-load' },
{ key: 'cert', label: '危化品证', anchor: 'plan-sec-cert' },
{ key: 'cert', label: '关联证件', anchor: 'plan-sec-cert' },
{ key: 'freight', label: '运费', anchor: 'plan-sec-freight' },
{ key: 'remark', label: '备注', anchor: 'plan-sec-remark' }
]
@@ -751,6 +811,7 @@
loadVersion++
actionVersion++
plan.value = null
linkedCertRows.value = []
readDetail.value = null
partialErrors.value = []
errorText.value = ''
@@ -807,9 +868,12 @@
readDetail.value = detail
partialErrors.value = [...detail.errors, ...(arrangeFailed ? ['安排信息'] : [])]
plan.value = data
await loadLinkedCerts(id)
if (!current()) return
} catch {
if (!current()) return
plan.value = null
linkedCertRows.value = []
resetArrangeActions()
errorText.value = '计划详情加载失败或记录不存在,请重试'
} finally {
@@ -1516,6 +1580,57 @@
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 12px;
}
.plan-linked-certs {
display: flex;
flex-direction: column;
gap: 12px;
}
.plan-linked-cert-block {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
border: 1px solid #d9e8e2;
border-radius: 10px;
background: #fff;
}
.plan-linked-cert-block__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
}
.plan-linked-cert-block__no {
font-size: 14px;
font-weight: 600;
color: #1e293b;
word-break: break-all;
}
.plan-linked-cert-block__slot {
display: flex;
flex-direction: column;
gap: 8px;
}
.plan-linked-cert-block__missing {
display: flex;
align-items: center;
justify-content: center;
width: 140px;
height: 96px;
margin: 0;
border: 1px dashed #d9e8e2;
border-radius: 8px;
background: #f8fafc;
color: #64748b;
font-size: 12px;
}
.mp-plan-quota-sheet {