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 + 表格态四字段必填星号 |
Files:
web-admin/src/utils/pl-plan-stop-product-arrange-required.tsCreate: 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行:计划数量'
)
})
})
创建 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
}
Run:
cd web-admin && pnpm exec tsx --test src/utils/pl-plan-stop-product-arrange-required.test.ts
Expected: all tests PASS
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
)"
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'
validateContractsForArrange)/** 安排运输前:装/送货产品单位、计划数量、包装规格、送货方式必填 */
function validateStopProductsForArrange() {
const gap = findFirstStopProductArrangeGap({
loadProducts: loadForm.value.products,
deliveries: deliveryForms.value
})
if (gap) {
ElMessage.warning(gap)
return false
}
return true
}
handleSubmit 安排分支调用将安排分支末尾从:
if (!validateContractsForArrange()) return
改为:
if (!validateContractsForArrange()) return
if (!validateStopProductsForArrange()) return
确认:仅在 mode === 'saveAndArrange' || mode === 'rearrange' 内调用;mode === 'save' 不调用。
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
)"
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>。
把对应 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 等属性)。
在同文件 <style scoped> 末尾增加:
.pl-stop-field-required {
margin-left: 2px;
color: #dc2626;
font-weight: 600;
}
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
)"
/#/pl/plan-module/plan)*。0,点「保存」:应成功(不因这四项拦截)。安排运输前请完善… 提示,不发起安排。./scripts/update-graph.sh frontend
git add web-admin/graphify-out
git commit -m "$(cat <<'EOF'
chore: update web-admin graphify after arrange-required product fields
EOF
)"
(无变更则跳过本步。)
| Spec 要求 | Task |
|---|---|
| 仅安排硬拦、保存不拦 | Task 2 |
| 四字段规则 + qty>0 | Task 1 |
| 装货 + 各送货段 | Task 1 / 2 |
| 第一处缺失即 warning | Task 1 / 2 |
| 标签必填星号 | Task 3 |
| 不改后端 / 列表预检 | 未列入任务(YAGNI) |