|
|
@@ -0,0 +1,285 @@
|
|
|
+# 运输计划「复制」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:** 在运输计划列表支持「复制」:打开新增弹窗并预填可复制字段,用户保存后走现有 `createFull` 建新单。
|
|
|
+
|
|
|
+**Architecture:** 纯前端。列表传入 `copyFromPlanId`(`planId` 为空);`PlPlanFormDialog` 打开时 `planApi.get` → `loadPlanForCopy`(裁剪车辆/承运商/运费/执行态);保存路径与普通新增相同。
|
|
|
+
|
|
|
+**Tech Stack:** Vue 3 + Element Plus + 现有 `planApi`
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-08-06-pl-plan-copy-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 操作 | 文件 | 职责 |
|
|
|
+|------|------|------|
|
|
|
+| Modify | `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | `copyFromPlanId`、`loadPlanForCopy`、复制态禁用日期联动、产品映射去证书/源 id |
|
|
|
+| Modify | `web-admin/src/views/pl/plan/index.vue` | 工具栏 + 查看弹窗「复制」入口 |
|
|
|
+| Modify | `docs/superpowers/specs/2026-08-06-pl-plan-copy-design.md` | 状态改为已实现 |
|
|
|
+
|
|
|
+无后端改动。web-admin 无组件单测基建,验收以浏览器走查为准。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 表单弹窗支持按源计划预填(复制模式)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/business/pl-plan-form-dialog/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展 props 与复制态计算属性**
|
|
|
+
|
|
|
+在现有 props 上增加 `copyFromPlanId`:
|
|
|
+
|
|
|
+```ts
|
|
|
+const props = defineProps<{
|
|
|
+ visible: boolean
|
|
|
+ planId?: string
|
|
|
+ /** 有值且无 planId 时:新增模式 + 从该计划预填(复制) */
|
|
|
+ copyFromPlanId?: string
|
|
|
+}>()
|
|
|
+
|
|
|
+const isEdit = computed(() => !!props.planId)
|
|
|
+const isCopy = computed(() => !props.planId && !!props.copyFromPlanId)
|
|
|
+```
|
|
|
+
|
|
|
+`dialogTitle` / `saveButtonLabel` / `showSaveAndArrange` 继续按 `isEdit` 判断(复制视为新增,标题「新增运输计划」)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 复制专用产品行映射**
|
|
|
+
|
|
|
+在 `mapPlanProductToFormRow` 旁新增(或改其调用方),保证复制行:
|
|
|
+
|
|
|
+- 不带源 `plan_product.id`(`id` 省略)
|
|
|
+- `actualQty: 0`
|
|
|
+- 不带 `purchaseCertUrl` / `transportCertUrl` / `legalUseCertUrl`
|
|
|
+- `quantity` 仍用计划数量(与现有 `mapPlanProductToFormRow` 的 quantity 逻辑一致)
|
|
|
+
|
|
|
+```ts
|
|
|
+function mapPlanProductToCopyFormRow(item: PlanProduct, index: number): PlanFormProductRow {
|
|
|
+ const row = mapPlanProductToFormRow(item, index)
|
|
|
+ return {
|
|
|
+ ...row,
|
|
|
+ id: undefined,
|
|
|
+ actualQty: 0,
|
|
|
+ purchaseCertUrl: undefined,
|
|
|
+ transportCertUrl: undefined,
|
|
|
+ legalUseCertUrl: undefined
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现 `loadPlanForCopy`**
|
|
|
+
|
|
|
+以 `loadPlanForEdit` 为模板,差异如下:
|
|
|
+
|
|
|
+1. `resetForm()` 后 `planApi.get(planId)`
|
|
|
+2. `Object.assign` 时:
|
|
|
+ - `createDate: todayYmd()`(不用源创建日期)
|
|
|
+ - `followUserId` / `followUserName`:`resolveCurrentUserId()` / `resolveCurrentUserName()`
|
|
|
+ - `planDate`、取送货字段、备注、合同相关字段:同编辑(时间用现有 `formatDate`)
|
|
|
+ - **不要**写入 `vehicleId`
|
|
|
+ - `products: (data.products || []).map(mapPlanProductToCopyFormRow)`
|
|
|
+3. `loadedTransportStatus.value = 'pending'`(新建态)
|
|
|
+4. 合同快照加载逻辑同编辑(`getContract` 失败则 `buildContractSnapshotFromPlan`)
|
|
|
+5. 产品 enrich 同编辑:`enrichProductsFromContract` → `enrichPlanProductsHazardFromCrm` → `enrichPlanProductLinesAlias` → `refreshContractProductRows`
|
|
|
+6. **不要**调用 `ensureSelectedVehicleOption` / `handleVehicleChange` / `restoreFreightFromTransport` / `ensureSelectedCarrierOption`
|
|
|
+7. 显式保持:`formData.vehicleId = ''`、`formData.carrierId = ''`、`selectedDriverId = ''`、`vehicleBundle = null`,并 `freightCalc.resetFreightCalc()`
|
|
|
+
|
|
|
+失败处理:
|
|
|
+
|
|
|
+```ts
|
|
|
+} catch (e) {
|
|
|
+ const message = e instanceof Error ? e.message : '加载源计划失败'
|
|
|
+ ElMessage.error(message.includes('加载') ? message : `加载源计划失败:${message}`)
|
|
|
+ emit('update:visible', false)
|
|
|
+} finally {
|
|
|
+ detailLoading.value = false
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 接入 `handleOpened` 与日期 watch**
|
|
|
+
|
|
|
+`handleOpened`:
|
|
|
+
|
|
|
+```ts
|
|
|
+function handleOpened() {
|
|
|
+ loadMasterData().then(async () => {
|
|
|
+ if (props.planId) {
|
|
|
+ await loadPlanForEdit(props.planId)
|
|
|
+ } else if (props.copyFromPlanId) {
|
|
|
+ await loadPlanForCopy(props.copyFromPlanId)
|
|
|
+ } else {
|
|
|
+ resetForm()
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+计划日期联动 watch 开头增加复制跳过:
|
|
|
+
|
|
|
+```ts
|
|
|
+watch(
|
|
|
+ () => formData.planDate,
|
|
|
+ (next, prev) => {
|
|
|
+ if (isEdit.value || isCopy.value) return
|
|
|
+ // ...现有联动逻辑
|
|
|
+ }
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 自测表单组件逻辑(代码审查清单)**
|
|
|
+
|
|
|
+确认:
|
|
|
+
|
|
|
+- `isCopy` 时提交仍走 `!props.planId` → `createFull`
|
|
|
+- `buildTransportBlock` 在无车辆时不带 transport(同新增)
|
|
|
+- `handleClosed` 仍 `resetForm()` 即可(父组件负责清空 `copyFromPlanId`)
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/business/pl-plan-form-dialog/index.vue
|
|
|
+git commit -m "feat(pl): support copy-from plan prefill in plan form dialog"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 列表页「复制」入口
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/pl/plan/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 状态与表单绑定**
|
|
|
+
|
|
|
+增加:
|
|
|
+
|
|
|
+```ts
|
|
|
+const copyFromPlanId = ref('')
|
|
|
+```
|
|
|
+
|
|
|
+模板中表单:
|
|
|
+
|
|
|
+```vue
|
|
|
+<PlPlanFormDialog
|
|
|
+ v-model:visible="formDialogVisible"
|
|
|
+ :plan-id="editingPlanId"
|
|
|
+ :copy-from-plan-id="copyFromPlanId"
|
|
|
+ @success="handlePlanMutationSuccess"
|
|
|
+/>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 统一打开表单的互斥赋值**
|
|
|
+
|
|
|
+```ts
|
|
|
+function handleAdd() {
|
|
|
+ editingPlanId.value = ''
|
|
|
+ copyFromPlanId.value = ''
|
|
|
+ formDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleEdit(row: Plan) {
|
|
|
+ editingPlanId.value = row.id
|
|
|
+ copyFromPlanId.value = ''
|
|
|
+ formDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleCopy(row: Plan) {
|
|
|
+ editingPlanId.value = ''
|
|
|
+ copyFromPlanId.value = row.id
|
|
|
+ formDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function handleToolbarCopy() {
|
|
|
+ if (!selectedPlan.value) return
|
|
|
+ handleCopy(selectedPlan.value)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 工具栏按钮**
|
|
|
+
|
|
|
+在「新增计划」后增加(`btn-ghost`,仅单选可点):
|
|
|
+
|
|
|
+```vue
|
|
|
+<el-button
|
|
|
+ class="btn-ghost"
|
|
|
+ :disabled="!selectedPlan"
|
|
|
+ @click="handleToolbarCopy"
|
|
|
+>
|
|
|
+ <el-icon><CopyDocument /></el-icon>复制
|
|
|
+</el-button>
|
|
|
+```
|
|
|
+
|
|
|
+`CopyDocument` 加入 `@element-plus/icons-vue` 的 import。
|
|
|
+
|
|
|
+- [ ] **Step 4: 查看弹窗 hero-actions**
|
|
|
+
|
|
|
+在「编辑」按钮旁增加「复制」(**无** `canEditPlan` 限制,任意状态可复制):
|
|
|
+
|
|
|
+```vue
|
|
|
+<el-button class="btn-ghost" @click="handleCopy(viewingPlan)">
|
|
|
+ <el-icon><CopyDocument /></el-icon>复制
|
|
|
+</el-button>
|
|
|
+```
|
|
|
+
|
|
|
+放在编辑按钮之后(或编辑之前;与编辑并列即可)。不强制关闭查看弹窗。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/pl/plan/index.vue
|
|
|
+git commit -m "feat(pl): add copy entry on transport plan list and view dialog"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 文档收尾与验收
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `docs/superpowers/specs/2026-08-06-pl-plan-copy-design.md`
|
|
|
+- 本计划文件勾选完成项
|
|
|
+
|
|
|
+- [ ] **Step 1: 设计文档状态**
|
|
|
+
|
|
|
+将文首 `**状态:** 设计已确认` 改为 `**状态:** 已实现`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 更新知识图谱**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh frontend
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 浏览器走查**
|
|
|
+
|
|
|
+打开 `http://localhost:3006/#/pl/plan-module/plan`:
|
|
|
+
|
|
|
+1. 勾选 1 条 →「复制」→ 标题「新增运输计划」;创建日期为今天;跟进人为当前用户
|
|
|
+2. 合同/基础/产品计划数量/备注/取送货已填;车牌、承运商为空
|
|
|
+3. 保存 → 新编号、待安排;源计划不变
|
|
|
+4. 对已作废或已归档计划从查看弹窗复制,可打开预填
|
|
|
+5. 0 条 / 多条勾选时工具栏「复制」禁用;编辑入口仍正常
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-08-06-pl-plan-copy-design.md docs/superpowers/plans/2026-08-06-pl-plan-copy.md
|
|
|
+git commit -m "docs: mark transport plan copy design implemented"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec 覆盖自检
|
|
|
+
|
|
|
+| Spec 要求 | Task |
|
|
|
+|-----------|------|
|
|
|
+| 打开新增弹窗预填,保存才建单 | Task 1 + 2 |
|
|
|
+| 列表工具栏单选 + 查看弹窗复制 | Task 2 |
|
|
|
+| 任意状态可复制 | Task 2 Step 4(无状态门禁) |
|
|
|
+| createDate=当天,跟进人=当前用户 | Task 1 Step 3 |
|
|
|
+| 计划/取送货日期原样 | Task 1 Step 3 |
|
|
|
+| 不复制车辆/承运商/运费 | Task 1 Step 3 |
|
|
|
+| 产品仅计划数量,无证书 URL / actualQty | Task 1 Step 2 |
|
|
|
+| 复制态关闭日期联动 watch | Task 1 Step 4 |
|
|
|
+| 拉源失败关弹窗 | Task 1 Step 3 |
|
|
|
+| 无新 API | 全任务 |
|