docs: add implementation plan for plan product arrange-required fields

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-30 17:51:10 +08:00
1 parent 5fbb15611c
commit 79b33c6ba1
1 file changed
+353
@@ -0,0 +1,353 @@
# 运输计划产品字段安排前必填 — 实现计划
> **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`:
```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`:
```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:
```bash
cd web-admin && pnpm exec tsx --test src/utils/pl-plan-stop-product-arrange-required.test.ts
```
Expected: all tests PASS
- [ ] **Step 4: Commit**
```bash
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 旁增加:
```ts
import { findFirstStopProductArrangeGap } from '@/utils/pl-plan-stop-product-arrange-required'
```
- [ ] **Step 2: 增加包装函数(紧挨 `validateContractsForArrange`)**
```ts
/** 安排运输前:装/送货产品单位、计划数量、包装规格、送货方式必填 */
function validateStopProductsForArrange() {
const gap = findFirstStopProductArrangeGap({
loadProducts: loadForm.value.products,
deliveries: deliveryForms.value
})
if (gap) {
ElMessage.warning(gap)
return false
}
return true
}
```
- [ ] **Step 3: 在 `handleSubmit` 安排分支调用**
将安排分支末尾从:
```ts
if (!validateContractsForArrange()) return
```
改为:
```ts
if (!validateContractsForArrange()) return
if (!validateStopProductsForArrange()) return
```
确认:仅在 `mode === 'saveAndArrange' || mode === 'rearrange'` 内调用;`mode === 'save'` 不调用。
- [ ] **Step 4: Commit**
```bash
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 改为(示例):
```html
<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,例如:
```vue
<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>` 末尾增加:
```scss
.pl-stop-field-required {
margin-left: 2px;
color: #dc2626;
font-weight: 600;
}
```
- [ ] **Step 4: Commit**
```bash
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: 更新知识图谱**
```bash
./scripts/update-graph.sh frontend
```
- [ ] **Step 3: 若有图谱变更则提交**
```bash
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) |