This commit is contained in:
zlwu committed 2026-07-20 10:50:26 +08:00
1 parent 6d9354a194
commit aff71d296b
2759 files changed
+860425 -13211

No files matched your search

@@ -0,0 +1,39 @@
# PL 入库单 / 出库单 Implementation Plan
> **For agentic workers:** Execute task-by-task. Spec: `docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md`
**Goal:** 安排运输后落 PL 入库/出库主从表;一单一链免登录填实际数量;管理端库存管理菜单列表编辑;作废计划作废单据;已安排计划冻结(无调整运输)。
**Architecture:** `pl_inbound_order`/`pl_outbound_order` + item;安排运输时创建(替代新 `pl_transport_site_link`);地磅数量仅预填;公共页 `/public/pl/inbound|outbound/{token}`。
**Tech Stack:** FastAPI + SQLAlchemy + Vue3/Element Plus;菜单 `sys_menu`;Fresh Mint UI。
---
## File map
| 职责 | 路径 |
|------|------|
| DDL 迁移 | `backend/scripts/migration_20260719_pl_inbound_outbound_order.sql` |
| 模型 | `backend/app/models/pl_models.py` |
| 服务 | `backend/app/services/pl_io_order_service.py` |
| Schema | `backend/app/schemas/pl/io_order.py` |
| 管理 API | `backend/app/api/pl/io_order.py` |
| 公共 API | `backend/app/api/pl/public_io_order.py` |
| 安排/作废接入 | `backend/app/api/pl/transport.py`, `plan.py` |
| 路由注册 | `backend/app/api/pl/__init__.py`, `main.py` |
| 菜单 | 同上 migration SQL;`web-admin/src/router/modules/pl.ts` |
| 前端 API | `web-admin/src/api/pl/index.ts` |
| 列表/弹窗 | `web-admin/src/views/pl/inbound/index.vue`, `outbound/index.vue` |
| 公共页 | `web-admin/src/views/public/pl/inbound/index.vue`, `outbound/...` |
| 计划冻结 UX | `web-admin/src/views/pl/plan/index.vue`, arrange dialog |
### Task 1: DDL + Models + Menu SQL
### Task 2: pl_io_order_service + arrange/cancel wire + block rearrange
### Task 3: Admin + Public APIs
### Task 4: Frontend lists/dialogs/public/freeze/archive
### Task 5: graphify update + smoke check
---
**执行方式:** 本会话 Inline 连续实现(用户已确认「继续」)。
@@ -0,0 +1,360 @@
# 运输计划 · 保存并安排运输 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:
```ts
import {
enrichPlanProductsHazardFromCrm,
buildMissingHazardCertReadyMessage
} from '@/composables/usePlToxicProductCertCheck'
import { pickBxData } from '@/utils/http/bx'
```
(注意:文件已 import `enrichPlanProductsHazardFromCrm`,合并进同一条 import,勿重复。)
3. 将 emit 改为:
```ts
const emit = defineEmits<{
'update:visible': [value: boolean]
success: [payload?: { planId: string; openArrange: boolean }]
}>()
```
4. 增加 ref,并在 `resetForm` 中清零;在 `loadPlanForEdit` 赋值:
```ts
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)
```
5. 增加计算属性:
```ts
const showSaveAndArrange = computed(
() => !isEdit.value || loadedTransportStatus.value === 'pending'
)
const saveButtonLabel = computed(() => (isEdit.value ? '保存' : '创建'))
const saveAndArrangeLabel = computed(() =>
isEdit.value ? '保存并安排运输' : '创建并安排运输'
)
```
- [ ] **Step 2: 改 template footer**
将现有 `#footer` 替换为:
```vue
<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` 不变):
```ts
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(若用户要求提交时再执行)**
```bash
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`**
将:
```ts
async function handlePlanMutationSuccess() {
await fetchData()
await refreshViewIfOpen()
}
```
替换为:
```ts
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(若用户要求提交时再执行)**
```bash
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 行的编辑按钮块:
```vue
<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` 中删除编辑按钮(保留归档按钮),即删除:
```vue
<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(若用户要求提交时再执行)**
```bash
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)**
```bash
./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(勿做)
- 后端合并接口、表单内嵌安排区、「保存并调整运输」、修改安排弹窗内部逻辑。
@@ -0,0 +1,121 @@
# 运输计划 · 保存并安排运输(设计)
**日期:** 2026-07-19
**状态:** 已实现
**范围:** `web-admin` 运输计划新增/编辑弹窗快捷入口;运输计划归档页隐藏编辑
## 背景
运输计划新增/编辑弹窗(`PlPlanFormDialog`)当前底栏仅有「取消」与「创建/保存」。创建计划与安排运输是两步:保存后需回列表再点「安排运输」。
当计划产品不含易制毒/易制爆(或证书已齐备)时,用户希望保存后立刻进入安排运输,减少往返。
后端 `create-full` 明确「不自动安排运输」;安排运输仍由独立弹窗与 API 完成。本需求不改变该边界,仅做前端串联。
## 目标
1. 在新增/编辑弹窗增加「创建/保存并安排运输」主按钮,保存成功后自动打开现有「安排运输」弹窗。
2. 保留原「创建/保存」,供需补证书或稍后安排的场景。
3. 运输计划归档页不再提供编辑运输计划入口。
## 非目标
- 不新增后端「创建并安排」合并接口
- 不把安排运输表单内嵌进计划表单
- 不提供「保存并调整运输」入口(已安排后不显示该快捷按钮)
- 不修改安排运输弹窗内部逻辑、危化品证上传页逻辑
## 决策摘要
| 编号 | 决策 | 说明 |
|------|------|------|
| D1 | 底栏方案 A | 取消 · 创建/保存(描边)· 创建/保存并安排运输(主色) |
| D2 | 实现路径 1 | 前端串联:保存 → 可选打开现有安排弹窗;无新 API |
| D3 | 证书未齐备 | 仍保存计划,提示无法安排的原因,不打开安排弹窗 |
| D4 | 显隐 | 新增始终显示「并安排」;编辑仅 `transportStatus === 'pending'` 时显示 |
| D5 | 取消安排 | 计划已保存,状态仍为「待安排」,可稍后在列表再安排 |
| D6 | 归档页 | 隐藏所有「编辑」入口,不打开计划表单弹窗 |
## 交互设计
### 底栏
| 场景 | 底栏按钮 |
|------|----------|
| 新增 | 取消 · 创建(描边)· **创建并安排运输**(主色) |
| 编辑且未安排 | 取消 · 保存(描边)· **保存并安排运输**(主色) |
| 编辑且已安排 | 取消 · **保存**(主色);不显示「并安排」 |
规则:
- 有「并安排」按钮时,它是唯一 `type="primary"`;「创建/保存」为中性描边。
- 无「并安排」时,「保存」恢复为主色(与现网一致)。
- 按钮须带 Element Plus 图标(与 OpenSpec UI 约定一致)。
### 「创建/保存并安排运输」流程
1. **校验**:与现有创建/保存相同(基础表单校验、已选合同、至少一条产品)。
2. **保存**:调用现有 `POST /transport/plan/create-full` 或 `PUT /transport/plan/{id}`,取得 `planId`。
3. **危化品门禁(前端)**:复用 `shouldBlockTransportForHazardCert` / `buildMissingHazardCertReadyMessage`(与安排弹窗一致)。
- 需拦截:成功提示保存完成 + 说明无法安排的原因 → 关闭表单 → **不**打开安排弹窗。
- 可通过:关闭表单 → 打开现有「安排运输」弹窗(`planId`)。
4. **安排弹窗**:用户照常填写并提交;若取消,计划保留为「待安排」(D5)。
### 「创建/保存」
行为与现网一致:仅保存,关闭弹窗,不打开安排运输。
### 运输计划归档页
- 隐藏工具栏「编辑」与查看弹窗 hero「编辑」。
- 可移除或闲置 `PlPlanFormDialog` 挂载(以实现时最小改动为准;至少保证无 UI 入口)。
- 归档/查看流程不变。
## 组件与数据流
```
PlPlanFormDialog
handleSubmit(mode: 'save' | 'saveAndArrange')
→ createFull / update
→ emit('success', { planId, openArrange: boolean })
plan/index.vue
handlePlanMutationSuccess(payload)
→ 刷新列表
→ if openArrange: 打开 PlPlanArrangeDialog(planId)
```
事件约定:
- 仅保存:`{ planId, openArrange: false }`(或等价兼容:无 payload 时视为仅刷新)。
- 保存并安排且通过门禁:`{ planId, openArrange: true }`。
- 保存并安排但被门禁拦截:`{ planId, openArrange: false }`(计划已保存)。
`planId` 来源:新建取接口返回的 `id`;编辑用当前 `planId`。
## 错误处理
| 情况 | 行为 |
|------|------|
| 表单校验失败 | 不提交,与现网一致 |
| 保存 API 失败 | 不关闭弹窗、不打开安排,展示错误 |
| 证书未齐备 | 保存成功后提示原因,不打开安排 |
| 安排弹窗内提交失败 | 由安排弹窗自行处理;计划仍为待安排或按安排 API 结果更新 |
## 验收要点
- V1:新增弹窗可见「创建」+「创建并安排运输」,主色在后者。
- V2:编辑未安排计划可见「保存并安排运输」;已安排则不可见。
- V3:普通品点「并安排」→ 保存成功 → 自动打开安排运输弹窗。
- V4:含危化品且证书未齐备 → 计划已保存 → 提示原因 → 不打开安排。
- V5:安排弹窗取消 → 列表中计划存在且为待安排。
- V6:运输计划归档页无编辑入口。
## 参考代码
- 表单:`web-admin/src/components/business/pl-plan-form-dialog/index.vue`
- 列表:`web-admin/src/views/pl/plan/index.vue`
- 安排弹窗:`web-admin/src/components/business/pl-plan-arrange-dialog/index.vue`
- 证书门禁:`web-admin/src/composables/usePlToxicProductCertCheck.ts`
- 归档页:`web-admin/src/views/pl/plan-archive/index.vue`
- 计划冻结(入库/出库已安排不可编辑):`isIoPlanFrozen`(列表既有逻辑,本需求不扩展调整运输)