2026-08-30-pl-plan-product-fields-arrange-required.md 10 KB

运输计划产品字段安排前必填 — 实现计划

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: 纯函数校验抽到 web-admin/src/utils/pl-plan-stop-product-arrange-required.ts(便于 node:test);pl-plan-form-dialog 在 saveAndArrange/rearrange 分支调用并 ElMessage.warning;pl-plan-stop-product-table 四字段标签加红色 *。不改后端与列表预检。

Tech Stack: Vue 3、Element Plus、TypeScript、node:test + pnpm exec tsx --test

Spec: docs/superpowers/specs/2026-08-30-pl-plan-product-fields-arrange-required-design.md


文件结构

文件 职责
Create: web-admin/src/utils/pl-plan-stop-product-arrange-required.ts 扫描装/送货产品行,返回第一处缺失提示文案(或 null)
Create: web-admin/src/utils/pl-plan-stop-product-arrange-required.test.ts 单元测试
Modify: web-admin/src/components/business/pl-plan-form-dialog/index.vue validateStopProductsForArrange + 安排分支调用
Modify: web-admin/src/components/business/pl-plan-stop-product-table/index.vue compact + 表格态四字段必填星号

Task 1: 纯函数校验 + 失败测试

Files:

  • Create: web-admin/src/utils/pl-plan-stop-product-arrange-required.ts
  • Create: web-admin/src/utils/pl-plan-stop-product-arrange-required.test.ts

  • [ ] Step 1: 写失败测试(先不实现 util,或先写空实现让断言失败)

创建 web-admin/src/utils/pl-plan-stop-product-arrange-required.test.ts:

import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import { findFirstStopProductArrangeGap } from './pl-plan-stop-product-arrange-required'

function row(partial: Record<string, unknown> = {}) {
  return {
    productId: 'p1',
    unit: '吨',
    planQty: 10,
    packageSpec: '槽罐',
    deliveryMethod: '送货上门',
    ...partial
  }
}

describe('findFirstStopProductArrangeGap', () => {
  it('returns null when load and deliveries are complete', () => {
    assert.equal(
      findFirstStopProductArrangeGap({
        loadProducts: [row()],
        deliveries: [{ products: [row()] }]
      }),
      null
    )
  })

  it('flags load product missing fields including planQty 0', () => {
    assert.equal(
      findFirstStopProductArrangeGap({
        loadProducts: [row({ unit: '', planQty: 0, packageSpec: ' ', deliveryMethod: null })],
        deliveries: []
      }),
      '安排运输前请完善装货产品第1行:单位、计划数量、包装规格、送货方式'
    )
  })

  it('flags delivery segment product and stops at first gap', () => {
    assert.equal(
      findFirstStopProductArrangeGap({
        loadProducts: [row()],
        deliveries: [
          { products: [row()] },
          { products: [row({ packageSpec: '' }), row({ deliveryMethod: '' })] }
        ]
      }),
      '安排运输前请完善送货段#2产品第1行:包装规格'
    )
  })

  it('treats null/undefined/NaN planQty as missing', () => {
    assert.equal(
      findFirstStopProductArrangeGap({
        loadProducts: [row({ planQty: undefined })],
        deliveries: []
      }),
      '安排运输前请完善装货产品第1行:计划数量'
    )
    assert.equal(
      findFirstStopProductArrangeGap({
        loadProducts: [row({ planQty: Number.NaN })],
        deliveries: []
      }),
      '安排运输前请完善装货产品第1行:计划数量'
    )
  })
})
  • Step 2: 写最小实现使测试通过

创建 web-admin/src/utils/pl-plan-stop-product-arrange-required.ts:

export type ArrangeRequiredProductLike = {
  unit?: string | null
  planQty?: number | null
  packageSpec?: string | null
  deliveryMethod?: string | null
}

export type ArrangeRequiredDeliveryLike = {
  products?: ArrangeRequiredProductLike[] | null
}

const FIELD_LABELS = {
  unit: '单位',
  planQty: '计划数量',
  packageSpec: '包装规格',
  deliveryMethod: '送货方式'
} as const

type FieldKey = keyof typeof FIELD_LABELS

function missingFields(row: ArrangeRequiredProductLike): FieldKey[] {
  const gaps: FieldKey[] = []
  if (!String(row.unit ?? '').trim()) gaps.push('unit')
  const qty = Number(row.planQty)
  if (!Number.isFinite(qty) || qty <= 0) gaps.push('planQty')
  if (!String(row.packageSpec ?? '').trim()) gaps.push('packageSpec')
  if (!String(row.deliveryMethod ?? '').trim()) gaps.push('deliveryMethod')
  return gaps
}

function formatGap(prefix: string, fields: FieldKey[]): string {
  return `${prefix}${fields.map((k) => FIELD_LABELS[k]).join('、')}`
}

/** 返回第一处产品字段缺失提示;齐全则 null */
export function findFirstStopProductArrangeGap(input: {
  loadProducts?: ArrangeRequiredProductLike[] | null
  deliveries?: ArrangeRequiredDeliveryLike[] | null
}): string | null {
  const loadProducts = input.loadProducts || []
  for (let i = 0; i < loadProducts.length; i += 1) {
    const fields = missingFields(loadProducts[i])
    if (fields.length) {
      return formatGap(`安排运输前请完善装货产品第${i + 1}行:`, fields)
    }
  }
  const deliveries = input.deliveries || []
  for (let d = 0; d < deliveries.length; d += 1) {
    const products = deliveries[d].products || []
    for (let i = 0; i < products.length; i += 1) {
      const fields = missingFields(products[i])
      if (fields.length) {
        return formatGap(`安排运输前请完善送货段#${d + 1}产品第${i + 1}行:`, fields)
      }
    }
  }
  return null
}
  • Step 3: 跑测试确认通过

Run:

cd web-admin && pnpm exec tsx --test src/utils/pl-plan-stop-product-arrange-required.test.ts

Expected: all tests PASS

  • Step 4: Commit
git add web-admin/src/utils/pl-plan-stop-product-arrange-required.ts \
  web-admin/src/utils/pl-plan-stop-product-arrange-required.test.ts
git commit -m "$(cat <<'EOF'
feat(pl): add arrange-required check for stop product fields

EOF
)"

Task 2: 弹窗安排分支接入校验

Files:

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

  • [ ] Step 1: 增加 import

在 script 既有 @/utils/pl-plan-stop-form 等 import 旁增加:

import { findFirstStopProductArrangeGap } from '@/utils/pl-plan-stop-product-arrange-required'
  • Step 2: 增加包装函数(紧挨 validateContractsForArrange)
/** 安排运输前:装/送货产品单位、计划数量、包装规格、送货方式必填 */
function validateStopProductsForArrange() {
  const gap = findFirstStopProductArrangeGap({
    loadProducts: loadForm.value.products,
    deliveries: deliveryForms.value
  })
  if (gap) {
    ElMessage.warning(gap)
    return false
  }
  return true
}
  • Step 3: 在 handleSubmit 安排分支调用

将安排分支末尾从:

if (!validateContractsForArrange()) return

改为:

if (!validateContractsForArrange()) return
if (!validateStopProductsForArrange()) return

确认:仅在 mode === 'saveAndArrange' || mode === 'rearrange' 内调用;mode === 'save' 不调用。

  • Step 4: Commit
git add web-admin/src/components/business/pl-plan-form-dialog/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): block arrange when stop product required fields missing

EOF
)"

Task 3: 产品表必填星号 UI

Files:

  • Modify: web-admin/src/components/business/pl-plan-stop-product-table/index.vue

  • [ ] Step 1: compact 态四字段标签加星号

将「单位」「计划数量」「包装规格」「送货方式」的 label 改为(示例):

<span class="pl-stop-product-dual-item__label"
  >单位<span class="pl-stop-field-required" aria-hidden="true">*</span></span
>

其余三字段同样加 <span class="pl-stop-field-required" aria-hidden="true">*</span>。

  • Step 2: 表格态四列表头加星号

把对应 el-table-column 的 label="单位" 等改为自定义 header,例如:

<el-table-column min-width="100" align="center">
  <template #header>
    <span>单位<span class="pl-stop-field-required" aria-hidden="true">*</span></span>
  </template>
  <template #default="{ row }">
    <!-- 保持原有单元格内容不变 -->
  </template>
</el-table-column>

对「计划数量」「包装规格」「送货方式」三列同样处理(保留原有 prop/min-width/align/v-if 等属性)。

  • Step 3: 增加样式

在同文件 <style scoped> 末尾增加:

.pl-stop-field-required {
  margin-left: 2px;
  color: #dc2626;
  font-weight: 600;
}
  • Step 4: Commit
git add web-admin/src/components/business/pl-plan-stop-product-table/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): mark arrange-required product fields with asterisk

EOF
)"

Task 4: 手工验收 + graphify

  • Step 1: 手工验收(/#/pl/plan-module/plan)
  1. 打开新增/编辑:装/送货产品区四字段有红色 *。
  2. 四项留空或计划数量为 0,点「保存」:应成功(不因这四项拦截)。
  3. 同样数据点「保存并安排」或「调整运输」:出现 安排运输前请完善… 提示,不发起安排。
  4. 装货与送货段分别缺项时,提示带正确段号/行号。
  5. 四项填齐后安排可继续后续原有校验。
  • Step 2: 更新知识图谱
./scripts/update-graph.sh frontend
  • Step 3: 若有图谱变更则提交
git add web-admin/graphify-out
git commit -m "$(cat <<'EOF'
chore: update web-admin graphify after arrange-required product fields

EOF
)"

(无变更则跳过本步。)


Spec 覆盖自检

Spec 要求 Task
仅安排硬拦、保存不拦 Task 2
四字段规则 + qty>0 Task 1
装货 + 各送货段 Task 1 / 2
第一处缺失即 warning Task 1 / 2
标签必填星号 Task 3
不改后端 / 列表预检 未列入任务(YAGNI)