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 || '—' }}
+ 计划日期{{ plan.planDate || '—' }}
+
+ 归属公司{{ getPlPlanCompanyLabel(plan.belongsCompany) }}
+
+ 单据类型{{ typeLabel }}
+ 跟进人{{ plan.followUserName || '—' }}
+
-
+
+
+
-
-
-
-
-
+
+
+
+ {{ action.label }}
+
+
+
+
+ 主车号{{ 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()
})
+
+