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(勿做)
- 后端合并接口、表单内嵌安排区、「保存并调整运输」、修改安排弹窗内部逻辑。