docs: add implementation plan for plan product arrange-required fields
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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) |
|
||||
Reference in new issue
Block a user