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

feat(web-admin): add mobile plan detail and shared action guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 2 месяцев назад
Родитель
Сommit
1a5aa1c934

+ 34 - 0
web-admin/src/utils/pl-plan-action-guards.ts

@@ -0,0 +1,34 @@
+export function canEditPlan(row: { archiveStatus?: string; followStatus?: string }) {
+  return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
+}
+
+export function canArrangeTransport(row: {
+  transportStatus?: string
+  archiveStatus?: string
+  followStatus?: string
+}) {
+  return (
+    (row.transportStatus === 'pending' || row.transportStatus === 'arranged') &&
+    row.archiveStatus !== 'archived' &&
+    row.followStatus !== 'cancelled'
+  )
+}
+
+export function arrangeTransportLabel(row: { transportStatus?: string }) {
+  return row.transportStatus === 'arranged' ? '调整运输' : '安排运输'
+}
+
+export function canVoidPlan(row: { archiveStatus?: string; followStatus?: string }) {
+  return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
+}
+
+export function canWaitNotice(row: { followStatus?: string; archiveStatus?: string }) {
+  return (
+    (row.followStatus === 'pending_arrange' || row.followStatus === 'drafting') &&
+    row.archiveStatus !== 'archived'
+  )
+}
+
+export function canCancelWaitNotice(row: { followStatus?: string; archiveStatus?: string }) {
+  return row.followStatus === 'wait_notice' && row.archiveStatus !== 'archived'
+}

+ 151 - 9
web-admin/src/views/m/pl/plan/detail.vue

@@ -1,18 +1,160 @@
 <template>
   <MpWebviewShell>
-    <p class="mp-wv-placeholder">计划详情加载中…</p>
+    <div v-if="loading" class="mp-wv-empty">加载中…</div>
+    <div v-else-if="errorText" class="mp-wv-empty">{{ errorText }}</div>
+    <template v-else-if="plan">
+      <header class="mp-wv-header">
+        <div class="mp-wv-header__row">
+          <button class="mp-wv-header__back" type="button" @click="goList">← 列表</button>
+          <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>
+      </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">
+          等客户通知
+        </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>
+      </div>
+    </template>
   </MpWebviewShell>
 </template>
 
 <script setup lang="ts">
+  import { computed, onMounted, ref } from 'vue'
+  import { useRoute, useRouter } from 'vue-router'
+  import { ElMessage, ElMessageBox } from 'element-plus'
   import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
-</script>
+  import { planApi, type Plan } from '@/api/pl'
+  import { buildPlanCard } from '@/utils/mp-pl-plan-card'
+  import {
+    arrangeTransportLabel,
+    canArrangeTransport,
+    canEditPlan,
+    canVoidPlan,
+    canWaitNotice
+  } from '@/utils/pl-plan-action-guards'
+  import '@/styles/mp-webview.scss'
+
+  const route = useRoute()
+  const router = useRouter()
+  const loading = ref(true)
+  const acting = ref(false)
+  const errorText = ref('')
+  const plan = ref<Plan | null>(null)
+
+  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 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) : '安排运输'
+  )
+
+  async function loadDetail() {
+    const id = String(route.params.id || '')
+    if (!id) {
+      errorText.value = '缺少计划 ID'
+      loading.value = false
+      return
+    }
+    loading.value = true
+    try {
+      const res = await planApi.get(id)
+      plan.value = ((res as { data?: Plan }).data ?? res) as Plan
+      errorText.value = ''
+    } catch (error) {
+      errorText.value = error instanceof Error ? error.message : '加载失败'
+    } finally {
+      loading.value = false
+    }
+  }
+
+  function goList() {
+    void router.push('/m/pl/plan')
+  }
+
+  function goEdit() {
+    if (!plan.value) return
+    void router.push(`/m/pl/plan/${plan.value.id}/edit`)
+  }
 
-<style scoped>
-  .mp-wv-placeholder {
-    margin: 24px 0 0;
-    font-size: 14px;
-    color: #6b7b74;
-    text-align: center;
+  function goArrange() {
+    if (!plan.value) return
+    void router.push(`/m/pl/plan/${plan.value.id}/arrange`)
   }
-</style>
+
+  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 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
+    }
+  }
+
+  onMounted(() => {
+    void loadDetail()
+  })
+</script>

+ 8 - 31
web-admin/src/views/pl/plan/index.vue

@@ -870,6 +870,14 @@ import { useListTableViewportHeight } from '@/composables/useListTableViewportHe
   import { canBatchPrintSlip } from '@/utils/pl-batch-print'
   import { listDeliveryPrintCustomers } from '@/utils/pl-delivery-slip-print'
   import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
+  import {
+    arrangeTransportLabel,
+    canArrangeTransport,
+    canCancelWaitNotice,
+    canEditPlan,
+    canVoidPlan,
+    canWaitNotice
+  } from '@/utils/pl-plan-action-guards'
   import {
     buildPlPlanListSpanMethod,
     flattenPlanListRows,
@@ -1433,37 +1441,6 @@ tableEl: planTableWrapRef,
     handleCopy(selectedPlan.value)
   }
 
-  function canEditPlan(row: Plan) {
-    return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
-  }
-
-  function canArrangeTransport(row: Plan) {
-    return (
-      (row.transportStatus === 'pending' || row.transportStatus === 'arranged') &&
-      row.archiveStatus !== 'archived' &&
-      row.followStatus !== 'cancelled'
-    )
-  }
-
-  function arrangeTransportLabel(row: Plan) {
-    return row.transportStatus === 'arranged' ? '调整运输' : '安排运输'
-  }
-
-  function canVoidPlan(row: Plan) {
-    return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
-  }
-
-  function canWaitNotice(row: Plan) {
-    return (
-      (row.followStatus === 'pending_arrange' || row.followStatus === 'drafting') &&
-      row.archiveStatus !== 'archived'
-    )
-  }
-
-  function canCancelWaitNotice(row: Plan) {
-    return row.followStatus === 'wait_notice' && row.archiveStatus !== 'archived'
-  }
-
   function syncViewingPlan(updated: Plan) {
     if (viewingPlan.value?.id === updated.id) {
       viewingPlan.value = { ...viewingPlan.value, ...updated }