diff --git a/web-admin/src/views/m/pl/plan/detail.vue b/web-admin/src/views/m/pl/plan/detail.vue index be989b154..7b5081dd4 100644 --- a/web-admin/src/views/m/pl/plan/detail.vue +++ b/web-admin/src/views/m/pl/plan/detail.vue @@ -9,33 +9,222 @@ {{ statusLabel }}

{{ plan.planNo || '计划详情' }}

-
- {{ typeLabel }} · 计划日期 {{ plan.planDate || '—' }} -
+
{{ typeLabel }} · 计划日期 {{ plan.planDate || '—' }}
-
-
送货时间:{{ deliveryTimeText }}
-
车牌:{{ plate }}
-
人员:{{ people }}
-
跟进人:{{ plan.followUserName || '—' }}
-
{{ product }}
-
+
+
+

基础信息

+
计划编号{{ plan.planNo || '—' }}
+
计划日期{{ plan.planDate || '—' }}
+
+ 归属公司{{ getPlPlanCompanyLabel(plan.belongsCompany) }} +
+
单据类型{{ typeLabel }}
+
跟进人{{ plan.followUserName || '—' }}
+
-
- +
+

暂无车辆信息

+ +
+
+

{{ line.title }}

+
{{ line.text }}
+
+
+ + +
+

装送货信息

+
+
装货信息
+
装货客户{{ load?.customerName || '—' }}
+
装货时间{{ load?.loadTime || '—' }}
+
装货地址{{ load?.address || '—' }}
+
联系方式{{ load?.contact || '—' }}
+
注意事项{{ load?.notes || '—' }}
+
装货合同{{ loadContractText }}
+
地磅单{{ dibangText(load) }}
+
+
暂无产品
+
+
{{ p.productName }}{{ p.model ? ` · ${p.model}` : '' }}
+
+
单位 {{ p.unit || '—' }}
+
包装 {{ p.packageSpec || '—' }}
+
计划量 {{ p.planQty ?? '—' }}
+
+
+
+
+ +
+
送货信息
+
+ +
+ +

暂无送货段

+
+ +
+
装货合计 {{ loadQty }}
+
送货合计 {{ deliveryQty }}
+
+ {{ qtyBalanced ? '✓ 数量平衡' : '⚠ 装送数量不一致' }} +
+
+
+ +
+

危化品证书信息

+
+ {{ certSummaryText }} + {{ certReadyLabel }} +
+ +

上传与齐备确认在「危化品证上传」页操作。

+
+ +
+

运费信息

+
+ 是否有运输费用{{ getPlYesNoLabel(plan.hasFreight) }} +
+
运费金额(元){{ freightAmountText }}
+
运费备注{{ freightRemarkText }}
+
+ +
+

备注

+

{{ plan.remark?.trim() || '—' }}

+
+
+ +
+ - - - - -
+ + + + + + +
主车号{{ plan.licensePlate || '—' }}
+
挂车号{{ plan.trailerNo || '—' }}
+
司机{{ plan.driverName || '—' }}
+
押运{{ plan.escortName || '—' }}
+
人员摘要{{ plan.personnelSummary || '—' }}
+
@@ -45,15 +234,37 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import MpWebviewShell from '@/layouts/MpWebviewShell.vue' - import { planApi, type Plan } from '@/api/pl' + import MpBottomSheet from '@/components/mp/MpBottomSheet.vue' + import PlPlanHazardCertViewTable from '@/components/business/pl-plan-hazard-cert-view-table/index.vue' + import { + planApi, + type Plan, + type PlanArrangeInfo, + type PlanDelivery, + type PlanLoad, + type TransportSiteLink + } from '@/api/pl' import { buildPlanCard } from '@/utils/mp-pl-plan-card' + import { + splitMpPlanDetailActions, + type MpDetailActionKind + } from '@/utils/mp-pl-plan-detail-actions' import { arrangeTransportLabel, canArrangeTransport, + canCancelWaitNotice, canEditPlan, canVoidPlan, canWaitNotice } from '@/utils/pl-plan-action-guards' + import { + getPlPlanCompanyLabel, + getPlYesNoLabel + } from '@/utils/pl-display' + import { + isHazardCertReady, + isHazardProduct + } from '@/composables/usePlToxicProductCertCheck' import '@/styles/mp-webview.scss' const route = useRoute() @@ -62,26 +273,120 @@ const acting = ref(false) const errorText = ref('') const plan = ref(null) + const arrangeInfo = ref(null) + const deliveryIndex = ref(0) + const moreOpen = ref(false) + const vehicleOpen = ref(false) const card = computed(() => (plan.value ? buildPlanCard(plan.value) : null)) const statusLabel = computed(() => card.value?.statusLabel || '—') const statusTagType = computed(() => card.value?.statusTagType || 'info') const typeLabel = computed(() => card.value?.typeLabel || '—') const plate = computed(() => card.value?.plate || '—') - const people = computed(() => card.value?.people || '—') - const product = computed(() => card.value?.product || '—') - const deliveryTimeText = computed(() => { - const times = plan.value?.deliveryTimes || [] - if (times.length) return times.join('、') - return plan.value?.deliveryTime || '—' + const isArranged = computed(() => plan.value?.transportStatus === 'arranged') + const hasVehicle = computed(() => { + const p = plan.value + return !!(p?.licensePlate || p?.vehicleId) }) - const canArrange = computed(() => (plan.value ? canArrangeTransport(plan.value) : false)) - const canEdit = computed(() => (plan.value ? canEditPlan(plan.value) : false)) - const canVoid = computed(() => (plan.value ? canVoidPlan(plan.value) : false)) - const canWait = computed(() => (plan.value ? canWaitNotice(plan.value) : false)) - const arrangeLabel = computed(() => - plan.value ? arrangeTransportLabel(plan.value) : '安排运输' + const load = computed(() => plan.value?.load || null) + const deliveries = computed(() => plan.value?.deliveries || []) + const activeDelivery = computed(() => deliveries.value[deliveryIndex.value] || null) + const arrangeDriverUrl = computed(() => arrangeInfo.value?.driverUrl || '') + const siteLinks = computed(() => arrangeInfo.value?.siteRecords || []) + + const loadContractText = computed(() => { + const list = load.value?.contracts || [] + if (!list.length) return '—' + return list.map((c) => c.contractNo || c.contractId).filter(Boolean).join('、') || '—' + }) + const deliveryContractText = computed(() => { + const list = activeDelivery.value?.contracts || [] + if (!list.length) return '—' + return list.map((c) => c.contractNo || c.contractId).filter(Boolean).join('、') || '—' + }) + + function sumProducts(products?: { planQty?: number }[]) { + return (products || []).reduce((sum, row) => sum + Number(row.planQty || 0), 0) + } + const loadQty = computed(() => sumProducts(load.value?.products)) + const deliveryQty = computed(() => + deliveries.value.reduce((sum, d) => sum + sumProducts(d.products), 0) ) + const qtyBalanced = computed(() => Math.abs(loadQty.value - deliveryQty.value) < 1e-6) + + const hazardProducts = computed(() => (plan.value?.products || []).filter((p) => isHazardProduct(p))) + const certReady = computed(() => isHazardCertReady(plan.value)) + const certReadyLabel = computed(() => { + if (!hazardProducts.value.length) return '无需证书' + return certReady.value ? '已齐备' : '待确认' + }) + const certReadyTagType = computed(() => { + if (!hazardProducts.value.length) return 'info' + return certReady.value ? 'success' : 'warning' + }) + const certSummaryText = computed(() => { + if (!hazardProducts.value.length) return '本计划无易制毒/易制爆产品' + return `${hazardProducts.value.length} 个危化产品` + }) + + const freightAmountText = computed(() => { + if (plan.value?.hasFreight !== 1) return '—' + const n = Number(plan.value?.freightAmount) + return Number.isFinite(n) ? n.toFixed(2) : '—' + }) + const freightRemarkText = computed(() => { + if (plan.value?.hasFreight !== 1) return '—' + return plan.value?.freightRemark?.trim() || '—' + }) + + const feedbackLines = computed(() => { + const lines: { title: string; text: string }[] = [] + for (const link of siteLinks.value) { + const remark = String(link.siteFeedbackRemark || '').trim() + if (remark) { + lines.push({ + title: `现场回传 · ${link.productName || '现场'}`, + text: remark + }) + } + } + return lines + }) + + const actionSplit = computed(() => { + const p = plan.value + if (!p) return { primary: [], more: [] } + return splitMpPlanDetailActions({ + canEdit: canEditPlan(p), + canArrange: canArrangeTransport(p), + arrangeLabel: arrangeTransportLabel(p), + canWait: canWaitNotice(p), + canCancelWait: canCancelWaitNotice(p), + canVoid: canVoidPlan(p) + }) + }) + + function dibangText(stop: PlanLoad | PlanDelivery | null) { + if (!stop) return '—' + if (stop.generateDibang === 1) { + return `生成${stop.dibangDirection ? `(${stop.dibangDirection})` : ''}` + } + return '未生成' + } + + async function copyText(text: string, label: string) { + const value = text.trim() + if (!value) { + ElMessage.warning('无可复制内容') + return + } + try { + await navigator.clipboard.writeText(value) + ElMessage.success(`已复制${label}`) + } catch { + ElMessage.error('复制失败') + } + } async function loadDetail() { const id = String(route.params.id || '') @@ -94,7 +399,19 @@ try { const res = await planApi.get(id) plan.value = ((res as { data?: Plan }).data ?? res) as Plan + deliveryIndex.value = 0 errorText.value = '' + if (plan.value.transportStatus === 'arranged') { + try { + const infoRes = await planApi.getArrangeInfo(id) + arrangeInfo.value = ((infoRes as { data?: PlanArrangeInfo }).data ?? + infoRes) as PlanArrangeInfo + } catch { + arrangeInfo.value = null + } + } else { + arrangeInfo.value = null + } } catch (error) { errorText.value = error instanceof Error ? error.message : '加载失败' } finally { @@ -106,51 +423,66 @@ void router.push('/m/pl/plan') } - function goEdit() { - if (!plan.value) return - void router.push(`/m/pl/plan/${plan.value.id}/edit`) - } - - function goArrange() { - if (!plan.value) return - void router.push(`/m/pl/plan/${plan.value.id}/arrange`) - } - - function goCopy() { - if (!plan.value) return - void router.push({ path: '/m/pl/plan/new', query: { copyFrom: plan.value.id } }) - } - - function goPrint() { - if (!plan.value) return - void router.push(`/m/pl/plan/${plan.value.id}/print`) - } - - async function doWaitNotice() { - if (!plan.value) return - await ElMessageBox.confirm('确认将该计划标记为等客户通知?', '等客户通知', { - type: 'warning' - }) - acting.value = true - try { - const res = await planApi.waitNotice(plan.value.id) - plan.value = ((res as { data?: Plan }).data ?? res) as Plan - ElMessage.success('已标记为等客户通知') - } finally { - acting.value = false + async function runAction(kind: MpDetailActionKind) { + const p = plan.value + if (!p) return + moreOpen.value = false + if (kind === 'edit') { + void router.push(`/m/pl/plan/${p.id}/edit`) + return } - } - - async function doVoid() { - if (!plan.value) return - await ElMessageBox.confirm('确认作废该运输计划?此操作不可恢复。', '作废', { type: 'warning' }) - acting.value = true - try { - const res = await planApi.cancel(plan.value.id) - plan.value = ((res as { data?: Plan }).data ?? res) as Plan - ElMessage.success('已作废') - } finally { - acting.value = false + if (kind === 'arrange') { + void router.push(`/m/pl/plan/${p.id}/arrange`) + return + } + if (kind === 'copy') { + void router.push({ path: '/m/pl/plan/new', query: { copyFrom: p.id } }) + return + } + if (kind === 'print') { + void router.push(`/m/pl/plan/${p.id}/print`) + return + } + if (kind === 'wait') { + await ElMessageBox.confirm('确认将该计划标记为等客户通知?', '等客户通知', { + type: 'warning' + }) + acting.value = true + try { + const res = await planApi.waitNotice(p.id) + plan.value = ((res as { data?: Plan }).data ?? res) as Plan + ElMessage.success('已标记为等客户通知') + } finally { + acting.value = false + } + return + } + if (kind === 'cancel-wait') { + await ElMessageBox.confirm('确认取消等通知,恢复为待安排?', '取消等通知', { + type: 'warning' + }) + acting.value = true + try { + const res = await planApi.cancelWaitNotice(p.id) + plan.value = ((res as { data?: Plan }).data ?? res) as Plan + ElMessage.success('已取消等通知') + } finally { + acting.value = false + } + return + } + if (kind === 'void') { + await ElMessageBox.confirm('确认作废该运输计划?此操作不可恢复。', '作废运输计划', { + type: 'warning' + }) + acting.value = true + try { + const res = await planApi.cancel(p.id) + plan.value = ((res as { data?: Plan }).data ?? res) as Plan + ElMessage.success('已作废') + } finally { + acting.value = false + } } } @@ -158,3 +490,186 @@ void loadDetail() }) + +