Просмотр исходного кода

feat(web-admin): redesign mp transport plan detail sections.

Align mobile detail with demo IA including hazard cert slots and capped footer actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 1 месяц назад
Родитель
Сommit
bf81d9b77c
1 измененных файлов с 590 добавлено и 75 удалено
  1. 590 75
      web-admin/src/views/m/pl/plan/detail.vue

+ 590 - 75
web-admin/src/views/m/pl/plan/detail.vue

@@ -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>