2026-07-19-pl-plan-save-and-arrange.md 11 KB

运输计划 · 保存并安排运输 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在运输计划新增/编辑弹窗增加「创建/保存并安排运输」主按钮(前端串联打开安排弹窗),并在运输计划归档页隐藏编辑入口。

Architecture: 仅前端改动。PlPlanFormDialog 保存成功后通过 success({ planId, openArrange }) 通知列表页;列表页刷新后按需打开现有 PlPlanArrangeDialog。危化品门禁复用 buildMissingHazardCertReadyMessage(保存后 GET 计划详情再判断)。无新后端 API。

Tech Stack: Vue 3 + Element Plus + TypeScript;@/api/pl、@/utils/http/bx(pickBxData)、usePlToxicProductCertCheck。

Spec: docs/superpowers/specs/2026-07-19-pl-plan-save-and-arrange-design.md


File map

职责 路径
表单底栏 + 保存/保存并安排 web-admin/src/components/business/pl-plan-form-dialog/index.vue
列表接事件并打开安排弹窗 web-admin/src/views/pl/plan/index.vue
归档页隐藏编辑 web-admin/src/views/pl/plan-archive/index.vue
设计文档(已完成) docs/superpowers/specs/2026-07-19-pl-plan-save-and-arrange-design.md

Task 1: PlPlanFormDialog — 底栏显隐与双提交

Files:

  • Modify: web-admin/src/components/business/pl-plan-form-dialog/index.vue

  • [ ] Step 1: 扩展 emit 与编辑态运输状态

在 <script setup> 中:

  1. 将 Van 加入 @element-plus/icons-vue 的 import。
  2. 增加 import:
import {
  enrichPlanProductsHazardFromCrm,
  buildMissingHazardCertReadyMessage
} from '@/composables/usePlToxicProductCertCheck'
import { pickBxData } from '@/utils/http/bx'

(注意:文件已 import enrichPlanProductsHazardFromCrm,合并进同一条 import,勿重复。)

  1. 将 emit 改为:
const emit = defineEmits<{
  'update:visible': [value: boolean]
  success: [payload?: { planId: string; openArrange: boolean }]
}>()
  1. 增加 ref,并在 resetForm 中清零;在 loadPlanForEdit 赋值:
const loadedTransportStatus = ref('pending')
const loadedHazardCertReady = ref(0)

// resetForm 内:
loadedTransportStatus.value = 'pending'
loadedHazardCertReady.value = 0

// loadPlanForEdit 在 Object.assign 之后:
loadedTransportStatus.value = data.transportStatus || 'pending'
loadedHazardCertReady.value = Number(data.hazardCertReady ?? 0)
  1. 增加计算属性:
const showSaveAndArrange = computed(
  () => !isEdit.value || loadedTransportStatus.value === 'pending'
)

const saveButtonLabel = computed(() => (isEdit.value ? '保存' : '创建'))

const saveAndArrangeLabel = computed(() =>
  isEdit.value ? '保存并安排运输' : '创建并安排运输'
)
  • Step 2: 改 template footer

将现有 #footer 替换为:

<template #footer>
  <div class="dialog-footer">
    <el-button @click="innerVisible = false">
      <el-icon><Close /></el-icon>取消
    </el-button>
    <el-button
      :type="showSaveAndArrange ? 'default' : 'primary'"
      :class="showSaveAndArrange ? 'btn-ghost' : undefined"
      :loading="submitLoading"
      @click="handleSubmit('save')"
    >
      <el-icon><Check /></el-icon>{{ saveButtonLabel }}
    </el-button>
    <el-button
      v-if="showSaveAndArrange"
      type="primary"
      :loading="submitLoading"
      @click="handleSubmit('saveAndArrange')"
    >
      <el-icon><Van /></el-icon>{{ saveAndArrangeLabel }}
    </el-button>
  </div>
</template>
  • Step 3: 重写 handleSubmit

用下列实现替换现有 handleSubmit(保留 validateBusinessRules 不变):

async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
  const valid = await basicFormRef.value?.validate().catch(() => false)
  if (!valid || !validateBusinessRules()) return

  submitLoading.value = true
  try {
    const commonPayload = buildPlanCommonPayload({ allowClearTransport: isEdit.value })
    let planId = props.planId || ''

    if (isEdit.value) {
      if (!props.planId) return
      await planApi.update(props.planId, commonPayload)
      planId = props.planId
      ElMessage.success('保存成功')
    } else {
      const res = await planApi.createFull({
        ...commonPayload,
        contractId: formData.contractId
      } as Parameters<typeof planApi.createFull>[0])
      const created = pickBxData<Plan>(res)
      planId = String(created?.id || '')
      if (!planId) {
        ElMessage.error('创建成功但未返回计划 ID')
        return
      }
      ElMessage.success('创建成功')
    }

    let openArrange = false
    if (mode === 'saveAndArrange' && planId) {
      const detailRes = await planApi.get(planId)
      const plan = pickBxData<Plan>(detailRes)
      const rawProducts = Array.isArray(plan?.products) ? plan.products : []
      const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
      const certMessage = buildMissingHazardCertReadyMessage(plan, enrichedProducts)
      if (certMessage) {
        await ElMessageBox.alert(
          `${certMessage},请至【危化品证上传】页面操作。`,
          '无法安排运输',
          { type: 'warning' }
        )
        openArrange = false
      } else {
        openArrange = true
      }
    }

    innerVisible.value = false
    emit('success', { planId, openArrange })
  } finally {
    submitLoading.value = false
  }
}

说明:拦截文案与 pl-plan-arrange-dialog 保持一致;loadedHazardCertReady 供后续扩展,门禁以 GET 详情为准。

  • Step 4: 本地目视确认

Run: 打开 http://localhost:3006/#/pl/plan-module/plan,点「新增计划」,确认底栏为「取消 · 创建 · 创建并安排运输」(后者主色)。

Expected: 双按钮布局正确;未点提交即可。

  • Step 5: Commit(若用户要求提交时再执行)
git add web-admin/src/components/business/pl-plan-form-dialog/index.vue
git commit -m "$(cat <<'EOF'
feat(pl): add save-and-arrange action on transport plan form dialog

EOF
)"

Task 2: 运输计划列表 — 串联安排弹窗

Files:

  • Modify: web-admin/src/views/pl/plan/index.vue

  • [ ] Step 1: 扩展 handlePlanMutationSuccess

将:

async function handlePlanMutationSuccess() {
  await fetchData()
  await refreshViewIfOpen()
}

替换为:

async function handlePlanMutationSuccess(payload?: { planId?: string; openArrange?: boolean }) {
  await fetchData()
  await refreshViewIfOpen()
  if (payload?.openArrange && payload.planId) {
    arrangePlanId.value = payload.planId
    arrangeDialogVisible.value = true
  }
}

说明:PlPlanArrangeDialog 的 @success="handlePlanMutationSuccess" 无 payload,行为仍为仅刷新,兼容不变。

  • Step 2: 手动验收(对照 spec V1–V5)
用例 操作 Expected
V1 新增弹窗底栏 创建(描边)+ 创建并安排运输(主色)
V3 普通品「创建并安排运输」 保存后自动打开安排运输弹窗
V5 安排弹窗点取消 列表有该计划,运输状态待安排
V4 含危化品且证书未齐 计划已保存;Alert 说明原因;不打开安排
V2 编辑已安排(直销若仍可编辑) 无「保存并安排运输」,仅「保存」主色
  • Step 3: Commit(若用户要求提交时再执行)
git add web-admin/src/views/pl/plan/index.vue
git commit -m "$(cat <<'EOF'
feat(pl): open arrange dialog after plan save-and-arrange

EOF
)"

Task 3: 运输计划归档 — 隐藏编辑

Files:

  • Modify: web-admin/src/views/pl/plan-archive/index.vue

  • [ ] Step 1: 去掉工具栏编辑按钮

删除模板中约 99–106 行的编辑按钮块:

<el-button
  v-if="selectedPlan && selectedPlan.followStatus !== 'cancelled'"
  class="btn-ghost"
  :disabled="selectedRows.length !== 1"
  @click="handleToolbarEdit"
>
  <el-icon><Edit /></el-icon>编辑
</el-button>
  • Step 2: 去掉查看弹窗 hero「编辑」

在 PlPlanViewDialog 的 #hero-actions 中删除编辑按钮(保留归档按钮),即删除:

<el-button
  v-if="viewingPlan.followStatus !== 'cancelled'"
  class="btn-main"
  type="primary"
  @click="handleEdit(viewingPlan)"
>
  <el-icon><Edit /></el-icon>编辑
</el-button>

若删除后 hero 仅剩归档按钮,将归档按钮改为 type="primary" / class="btn-main"(保持一行一个主色)。

  • Step 3: 移除闲置表单弹窗与相关代码

删除:

  1. 模板中的 <PlPlanFormDialog ... />
  2. import PlPlanFormDialog ...
  3. import { Edit } from ...(若已无其它引用)
  4. formDialogVisible、editingPlanId
  5. handleEdit、handleToolbarEdit
  6. handlePlanMutationSuccess 中若仅服务于表单,可改为只服务归档弹窗的 refresh(保留对 archive dialog @success 的刷新逻辑)

核对:归档弹窗、查看弹窗仍正常工作。

  • Step 4: 手动验收 V6

Run: 打开 http://localhost:3006/#/pl/plan-module/plan-archive

Expected: 工具栏与查看弹窗均无「编辑」;归档/调整归档仍可用。

  • Step 5: Commit(若用户要求提交时再执行)
git add web-admin/src/views/pl/plan-archive/index.vue
git commit -m "$(cat <<'EOF'
fix(pl): hide edit transport plan on archive list page

EOF
)"

Task 4: 收尾

Files:

  • 无新文件;按需更新知识图谱

  • [ ] Step 1: 对照 spec 勾选覆盖

Spec Task
D1 底栏方案 A Task 1
D2 前端串联 Task 1–2
D3 证书未齐备仍保存 Task 1 Step 3
D4 显隐 pending Task 1 Step 1
D5 取消安排保留待安排 Task 2(行为天然满足)
D6 归档隐藏编辑 Task 3
V1–V6 Task 2 Step 2 + Task 3 Step 4
  • Step 2: 更新 graphify(仅改了 web-admin)
./scripts/update-graph.sh frontend

Expected: 脚本成功结束,无报错。

  • Step 3: 将 design 状态改为已实现(实现完成后)

在 docs/superpowers/specs/2026-07-19-pl-plan-save-and-arrange-design.md 将 **状态:** 待实现 改为 **状态:** 已实现。


Spec coverage self-check

  • D1–D6、V1–V6 均有对应 Task/步骤。
  • 无 TBD /「类似 Task N」占位。
  • success payload 形状在 Task 1 与 Task 2 一致:{ planId, openArrange }。
  • 归档页不再挂载表单,故无需处理归档页 openArrange。

Out of scope(勿做)

  • 后端合并接口、表单内嵌安排区、「保存并调整运输」、修改安排弹窗内部逻辑。