|
|
@@ -9,33 +9,222 @@
|
|
|
<el-tag :type="statusTagType" size="small">{{ statusLabel }}</el-tag>
|
|
|
</div>
|
|
|
<h1>{{ plan.planNo || '计划详情' }}</h1>
|
|
|
- <div class="mp-wv-card__sub" style="color: rgba(255, 255, 255, 0.9)">
|
|
|
- {{ typeLabel }} · 计划日期 {{ plan.planDate || '—' }}
|
|
|
- </div>
|
|
|
+ <div class="mp-wv-header__sub">{{ typeLabel }} · 计划日期 {{ plan.planDate || '—' }}</div>
|
|
|
</header>
|
|
|
|
|
|
- <section class="mp-wv-card">
|
|
|
- <div class="mp-wv-card__meta">送货时间:{{ deliveryTimeText }}</div>
|
|
|
- <div class="mp-wv-card__meta">车牌:{{ plate }}</div>
|
|
|
- <div class="mp-wv-card__meta">人员:{{ people }}</div>
|
|
|
- <div class="mp-wv-card__meta">跟进人:{{ plan.followUserName || '—' }}</div>
|
|
|
- <div class="mp-wv-card__product">{{ product }}</div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <div class="mp-wv-toolbar" style="flex-wrap: wrap">
|
|
|
- <button v-if="canArrange" class="mp-wv-btn mp-wv-btn--primary" type="button" @click="goArrange">
|
|
|
- {{ arrangeLabel }}
|
|
|
- </button>
|
|
|
- <button v-if="canEdit" class="mp-wv-btn" type="button" @click="goEdit">编辑计划</button>
|
|
|
- <button class="mp-wv-btn" type="button" @click="goCopy">复制计划</button>
|
|
|
- <button v-if="canWait" class="mp-wv-btn" type="button" :disabled="acting" @click="doWaitNotice">
|
|
|
- 等客户通知
|
|
|
+ <div class="mp-wv-detail-body">
|
|
|
+ <section class="mp-wv-section">
|
|
|
+ <h3>基础信息</h3>
|
|
|
+ <div class="mp-wv-kv"><span>计划编号</span><span>{{ plan.planNo || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>计划日期</span><span>{{ plan.planDate || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv">
|
|
|
+ <span>归属公司</span><span>{{ getPlPlanCompanyLabel(plan.belongsCompany) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="mp-wv-kv"><span>单据类型</span><span>{{ typeLabel }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>跟进人</span><span>{{ plan.followUserName || '—' }}</span></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="mp-wv-section">
|
|
|
+ <h3>运输信息</h3>
|
|
|
+ <template v-if="isArranged">
|
|
|
+ <div v-if="arrangeDriverUrl" class="mp-wv-link-block">
|
|
|
+ <div class="mp-wv-link-label">司机链接</div>
|
|
|
+ <div class="mp-wv-link-row">
|
|
|
+ <input class="mp-wv-link-input" :value="arrangeDriverUrl" readonly />
|
|
|
+ <button class="mp-wv-btn" type="button" @click="copyText(arrangeDriverUrl, '司机链接')">
|
|
|
+ 复制
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="siteLinks.length" class="mp-wv-link-block">
|
|
|
+ <div class="mp-wv-link-label">现场链接</div>
|
|
|
+ <div v-for="link in siteLinks" :key="link.siteLinkId" class="mp-wv-site-link">
|
|
|
+ <div class="mp-wv-card__meta">{{ link.productName || '现场链接' }}</div>
|
|
|
+ <div class="mp-wv-link-row">
|
|
|
+ <input class="mp-wv-link-input" :value="link.siteUrl || ''" readonly />
|
|
|
+ <button
|
|
|
+ class="mp-wv-btn"
|
|
|
+ type="button"
|
|
|
+ @click="copyText(link.siteUrl || '', link.productName || '现场链接')"
|
|
|
+ >
|
|
|
+ 复制
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div v-if="hasVehicle" class="mp-wv-vehicle-box">
|
|
|
+ <div><b>{{ plate }}</b></div>
|
|
|
+ <div class="mp-wv-card__meta">挂车 {{ plan.trailerNo || '—' }}</div>
|
|
|
+ <div class="mp-wv-card__meta">司机 {{ plan.driverName || '—' }} · 押运 {{ plan.escortName || '—' }}</div>
|
|
|
+ <button class="mp-wv-btn" type="button" style="width: 100%; margin-top: 8px" @click="vehicleOpen = true">
|
|
|
+ 查看车辆详情
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <p v-else class="mp-wv-card__meta">暂无车辆信息</p>
|
|
|
+
|
|
|
+ <div v-if="feedbackLines.length" class="mp-wv-feedback">
|
|
|
+ <div v-for="(line, idx) in feedbackLines" :key="idx" class="mp-wv-feedback-box">
|
|
|
+ <h4>{{ line.title }}</h4>
|
|
|
+ <div>{{ line.text }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="mp-wv-section">
|
|
|
+ <h3>装送货信息</h3>
|
|
|
+ <div class="mp-wv-stop">
|
|
|
+ <div class="mp-wv-stop-title">装货信息</div>
|
|
|
+ <div class="mp-wv-kv"><span>装货客户</span><span>{{ load?.customerName || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>装货时间</span><span>{{ load?.loadTime || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>装货地址</span><span>{{ load?.address || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>联系方式</span><span>{{ load?.contact || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>注意事项</span><span>{{ load?.notes || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>装货合同</span><span>{{ loadContractText }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>地磅单</span><span>{{ dibangText(load) }}</span></div>
|
|
|
+ <div class="mp-wv-prod-table">
|
|
|
+ <div v-if="!(load?.products || []).length" class="mp-wv-prod-empty">暂无产品</div>
|
|
|
+ <div v-for="(p, i) in load?.products || []" :key="p.id || i" class="mp-wv-prod-row">
|
|
|
+ <div class="mp-wv-prod-name">{{ p.productName }}{{ p.model ? ` · ${p.model}` : '' }}</div>
|
|
|
+ <div class="mp-wv-prod-grid">
|
|
|
+ <div>单位 <b>{{ p.unit || '—' }}</b></div>
|
|
|
+ <div>包装 <b>{{ p.packageSpec || '—' }}</b></div>
|
|
|
+ <div>计划量 <b>{{ p.planQty ?? '—' }}</b></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="mp-wv-stop">
|
|
|
+ <div class="mp-wv-stop-title">送货信息</div>
|
|
|
+ <div v-if="deliveries.length > 1" class="mp-wv-delivery-tabs">
|
|
|
+ <button
|
|
|
+ v-for="(d, idx) in deliveries"
|
|
|
+ :key="d.id || idx"
|
|
|
+ class="mp-wv-delivery-tab"
|
|
|
+ :class="{ active: deliveryIndex === idx }"
|
|
|
+ type="button"
|
|
|
+ @click="deliveryIndex = idx"
|
|
|
+ >
|
|
|
+ 送货段 {{ idx + 1 }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <template v-if="activeDelivery">
|
|
|
+ <div class="mp-wv-kv">
|
|
|
+ <span>送货客户</span><span>{{ activeDelivery.customerName || '—' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="mp-wv-kv">
|
|
|
+ <span>送货时间</span><span>{{ activeDelivery.deliveryTime || '—' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="mp-wv-kv"><span>送货地址</span><span>{{ activeDelivery.address || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>联系方式</span><span>{{ activeDelivery.contact || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>注意事项</span><span>{{ activeDelivery.notes || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>送货合同</span><span>{{ deliveryContractText }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>地磅单</span><span>{{ dibangText(activeDelivery) }}</span></div>
|
|
|
+ <div class="mp-wv-prod-table">
|
|
|
+ <div v-if="!(activeDelivery.products || []).length" class="mp-wv-prod-empty">暂无产品</div>
|
|
|
+ <div
|
|
|
+ v-for="(p, i) in activeDelivery.products || []"
|
|
|
+ :key="p.id || i"
|
|
|
+ class="mp-wv-prod-row"
|
|
|
+ >
|
|
|
+ <div class="mp-wv-prod-name">
|
|
|
+ {{ p.productName }}{{ p.model ? ` · ${p.model}` : '' }}
|
|
|
+ </div>
|
|
|
+ <div class="mp-wv-prod-grid">
|
|
|
+ <div>单位 <b>{{ p.unit || '—' }}</b></div>
|
|
|
+ <div>包装 <b>{{ p.packageSpec || '—' }}</b></div>
|
|
|
+ <div>计划量 <b>{{ p.planQty ?? '—' }}</b></div>
|
|
|
+ <div v-if="p.deliveryMethod">送货方式 <b>{{ p.deliveryMethod }}</b></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <p v-else class="mp-wv-card__meta">暂无送货段</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="mp-wv-qty">
|
|
|
+ <div>装货合计 <b>{{ loadQty }}</b></div>
|
|
|
+ <div>送货合计 <b>{{ deliveryQty }}</b></div>
|
|
|
+ <div :class="qtyBalanced ? 'is-ok' : ''">
|
|
|
+ {{ qtyBalanced ? '✓ 数量平衡' : '⚠ 装送数量不一致' }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="mp-wv-section">
|
|
|
+ <h3>危化品证书信息</h3>
|
|
|
+ <div class="mp-wv-cert-summary">
|
|
|
+ <span>{{ certSummaryText }}</span>
|
|
|
+ <el-tag :type="certReadyTagType" size="small">{{ certReadyLabel }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <PlPlanHazardCertViewTable :products="hazardProducts" />
|
|
|
+ <p class="mp-wv-cert-hint">上传与齐备确认在「危化品证上传」页操作。</p>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="mp-wv-section">
|
|
|
+ <h3>运费信息</h3>
|
|
|
+ <div class="mp-wv-kv">
|
|
|
+ <span>是否有运输费用</span><span>{{ getPlYesNoLabel(plan.hasFreight) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="mp-wv-kv"><span>运费金额(元)</span><span>{{ freightAmountText }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>运费备注</span><span>{{ freightRemarkText }}</span></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="mp-wv-section">
|
|
|
+ <h3>备注</h3>
|
|
|
+ <p class="mp-wv-remark">{{ plan.remark?.trim() || '—' }}</p>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="mp-wv-fab-bar">
|
|
|
+ <button
|
|
|
+ v-if="actionSplit.more.length"
|
|
|
+ class="mp-wv-btn mp-wv-btn-more"
|
|
|
+ type="button"
|
|
|
+ aria-label="更多"
|
|
|
+ @click="moreOpen = true"
|
|
|
+ >
|
|
|
+ ⋯
|
|
|
</button>
|
|
|
- <button class="mp-wv-btn" type="button" @click="goPrint">打印单据</button>
|
|
|
- <button v-if="canVoid" class="mp-wv-btn" type="button" :disabled="acting" @click="doVoid">
|
|
|
- 作废
|
|
|
+ <button
|
|
|
+ v-for="(action, idx) in actionSplit.primary"
|
|
|
+ :key="action.kind"
|
|
|
+ class="mp-wv-btn"
|
|
|
+ :class="{
|
|
|
+ 'mp-wv-btn--primary': idx === 0 || actionSplit.primary.length === 1,
|
|
|
+ 'is-danger': action.danger
|
|
|
+ }"
|
|
|
+ type="button"
|
|
|
+ :disabled="acting"
|
|
|
+ @click="runAction(action.kind)"
|
|
|
+ >
|
|
|
+ {{ action.label }}
|
|
|
</button>
|
|
|
</div>
|
|
|
+
|
|
|
+ <MpBottomSheet v-model="moreOpen" title="更多操作" half>
|
|
|
+ <button
|
|
|
+ v-for="action in actionSplit.more"
|
|
|
+ :key="action.kind"
|
|
|
+ class="mp-wv-filter-opt"
|
|
|
+ :class="{ 'is-danger': action.danger }"
|
|
|
+ type="button"
|
|
|
+ :disabled="acting"
|
|
|
+ @click="runAction(action.kind)"
|
|
|
+ >
|
|
|
+ {{ action.label }}
|
|
|
+ </button>
|
|
|
+ </MpBottomSheet>
|
|
|
+
|
|
|
+ <MpBottomSheet v-model="vehicleOpen" title="车辆详情">
|
|
|
+ <div class="mp-wv-kv"><span>主车号</span><span>{{ plan.licensePlate || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>挂车号</span><span>{{ plan.trailerNo || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>司机</span><span>{{ plan.driverName || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>押运</span><span>{{ plan.escortName || '—' }}</span></div>
|
|
|
+ <div class="mp-wv-kv"><span>人员摘要</span><span>{{ plan.personnelSummary || '—' }}</span></div>
|
|
|
+ </MpBottomSheet>
|
|
|
</template>
|
|
|
</MpWebviewShell>
|
|
|
</template>
|
|
|
@@ -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<Plan | null>(null)
|
|
|
+ const arrangeInfo = ref<PlanArrangeInfo | null>(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<TransportSiteLink[]>(() => 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,55 +423,253 @@
|
|
|
void router.push('/m/pl/plan')
|
|
|
}
|
|
|
|
|
|
- function goEdit() {
|
|
|
- if (!plan.value) return
|
|
|
- void router.push(`/m/pl/plan/${plan.value.id}/edit`)
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- function goArrange() {
|
|
|
- if (!plan.value) return
|
|
|
- void router.push(`/m/pl/plan/${plan.value.id}/arrange`)
|
|
|
+ onMounted(() => {
|
|
|
+ void loadDetail()
|
|
|
+ })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+ .mp-wv-header__sub {
|
|
|
+ margin-top: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(255, 255, 255, 0.9);
|
|
|
}
|
|
|
|
|
|
- function goCopy() {
|
|
|
- if (!plan.value) return
|
|
|
- void router.push({ path: '/m/pl/plan/new', query: { copyFrom: plan.value.id } })
|
|
|
+ .mp-wv-detail-body {
|
|
|
+ padding-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
- function goPrint() {
|
|
|
- if (!plan.value) return
|
|
|
- void router.push(`/m/pl/plan/${plan.value.id}/print`)
|
|
|
+ .mp-wv-link-block {
|
|
|
+ margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
- 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
|
|
|
- }
|
|
|
+ .mp-wv-link-label {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7b74;
|
|
|
}
|
|
|
|
|
|
- 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
|
|
|
- }
|
|
|
+ .mp-wv-link-row {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
- onMounted(() => {
|
|
|
- void loadDetail()
|
|
|
- })
|
|
|
-</script>
|
|
|
+ .mp-wv-link-input {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ height: 36px;
|
|
|
+ padding: 0 10px;
|
|
|
+ border: 1px solid #dcebe4;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-site-link + .mp-wv-site-link {
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-vehicle-box {
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid #dcebe4;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f7faf8;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-feedback {
|
|
|
+ margin-top: 10px;
|
|
|
+ display: grid;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-feedback-box {
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px dashed #dcebe4;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f7faf8;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-feedback-box h4 {
|
|
|
+ margin: 0 0 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #087f57;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-stop {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-stop-title {
|
|
|
+ margin-bottom: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-delivery-tabs {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ overflow-x: auto;
|
|
|
+ margin: 8px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-delivery-tab {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border: 1px solid #dcebe4;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #fff;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7b74;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-delivery-tab.active {
|
|
|
+ color: #fff;
|
|
|
+ background: #0a9f6d;
|
|
|
+ border-color: #0a9f6d;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-prod-table {
|
|
|
+ margin-top: 8px;
|
|
|
+ border: 1px solid #dcebe4;
|
|
|
+ border-radius: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-prod-empty,
|
|
|
+ .mp-wv-prod-row {
|
|
|
+ padding: 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ border-bottom: 1px solid #eef4f1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-prod-row:last-child {
|
|
|
+ border-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-prod-name {
|
|
|
+ margin-bottom: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #087f57;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-prod-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 4px 10px;
|
|
|
+ color: #6b7b74;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-prod-grid b {
|
|
|
+ color: #1f2a26;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-qty {
|
|
|
+ display: grid;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #f7faf8;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-qty .is-ok {
|
|
|
+ color: #087f57;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-cert-summary {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7b74;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-cert-hint {
|
|
|
+ margin: 10px 0 0;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7b74;
|
|
|
+ line-height: 1.45;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mp-wv-remark {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.55;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+ }
|
|
|
+
|
|
|
+ .is-danger {
|
|
|
+ color: #d14343 !important;
|
|
|
+ }
|
|
|
+</style>
|