659 lines
21 KiB
Markdown
659 lines
21 KiB
Markdown
# 运输计划归档 — 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:** 新增「运输计划归档」菜单与归档/调整弹窗,支持填写 actualQty、调整运费并归档;已归档计划仍可编辑与调整;原运输计划页移除归档入口。
|
||
|
||
**Architecture:** 后端在 `archive_plan` 单事务内编排产品数量同步 → 运费更新(含结算阻断)→ 首次归档改状态 / 已归档仅调数据;前端新建 `plan-archive` 列表 + `PlPlanArchiveDialog`(布局对齐 `PlPlanArrangeDialog`),列表逻辑抽取 `usePlPlanList` 与运输计划共用。
|
||
|
||
**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2;Vue 3 + Element Plus + `usePlFreightCalc`
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-06-25-pl-plan-archive-design.md`
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `backend/app/schemas/pl/plan.py` | `PlanArchiveBody` 等请求 Schema |
|
||
| `backend/app/services/pl_plan_service.py` | `apply_archive_product_quantities`、`apply_archive_freight`、`transport_has_locked_freight_settlement`、`record_plan_change` |
|
||
| `backend/app/api/pl/plan.py` | 增强 `archive_plan`;`update_plan` 允许已归档编辑 |
|
||
| `web-admin/src/api/pl/index.ts` | `PlanArchiveBody` 类型 + `planApi.archive(id, body?)` |
|
||
| `web-admin/src/composables/usePlPlanList.ts` | 列表查询/分页/筛选共享逻辑 |
|
||
| `web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue` | 新增 `editableActualQtyOnly` |
|
||
| `web-admin/src/components/business/pl-plan-archive-dialog/index.vue` | 归档/调整弹窗 |
|
||
| `web-admin/src/views/pl/plan-archive/index.vue` | 归档列表页 |
|
||
| `web-admin/src/router/modules/pl.ts` | 路由 |
|
||
| `docs/sql/shanxia2.0.sql` | 菜单 + 按钮权限 |
|
||
| `web-admin/src/utils/router/menu-icon.ts` | 菜单图标 |
|
||
| `web-admin/src/views/pl/plan/index.vue` | 删除批量归档 |
|
||
| `web-admin/src/views/pl/plan/detail.vue` | 删除归档按钮 |
|
||
|
||
---
|
||
|
||
### Task 1: 归档请求 Schema
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/schemas/pl/plan.py`
|
||
- Modify: `backend/app/schemas/pl/__init__.py`(若需 export)
|
||
|
||
- [ ] **Step 1:** 在 `plan.py` 末尾、`PlanListResponse` 之前追加:
|
||
|
||
```python
|
||
class PlanArchiveProductItem(BaseModel):
|
||
id: str = Field(..., description="计划产品明细ID pl_plan_product.id")
|
||
actual_qty: Decimal = Field(..., alias="actualQty", ge=0, description="实际数量")
|
||
model_config = {"populate_by_name": True}
|
||
|
||
|
||
class PlanArchiveFreightBlock(BaseModel):
|
||
distance_km: Optional[Decimal] = Field(default=None, alias="distanceKm")
|
||
freight_type: str = Field(default="by_weight", alias="freightType")
|
||
freight_price: Optional[Decimal] = Field(default=None, alias="freightPrice")
|
||
freight_amount: Optional[Decimal] = Field(default=None, alias="freightAmount")
|
||
transport_qty: Optional[Decimal] = Field(default=None, alias="transportQty")
|
||
has_invoice: Optional[int] = Field(default=None, alias="hasInvoice")
|
||
model_config = {"populate_by_name": True}
|
||
|
||
|
||
class PlanArchiveBody(BaseModel):
|
||
products: list[PlanArchiveProductItem] = Field(..., min_length=1)
|
||
freight: Optional[PlanArchiveFreightBlock] = None
|
||
model_config = {"populate_by_name": True}
|
||
```
|
||
|
||
- [ ] **Step 2:** 验证 import 无循环
|
||
|
||
Run: `cd backend && source venv/bin/activate && python -c "from app.schemas.pl.plan import PlanArchiveBody; print('ok')"`
|
||
Expected: `ok`
|
||
|
||
---
|
||
|
||
### Task 2: 归档业务 Service
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/services/pl_plan_service.py`
|
||
|
||
- [ ] **Step 1:** 新增 import:
|
||
|
||
```python
|
||
from app.models.pl_models import PlFreightSettlement, PlFreightSettlementTransport, PlPlanChange, PlTransport
|
||
from app.schemas.pl.plan import PlanArchiveFreightBlock, PlanArchiveProductItem
|
||
```
|
||
|
||
- [ ] **Step 2:** 追加 `transport_has_locked_freight_settlement`:
|
||
|
||
```python
|
||
_LOCKED_SETTLEMENT_STATUSES = ("submitted", "approved", "confirmed")
|
||
|
||
|
||
def transport_has_locked_freight_settlement(
|
||
db: Session, tenant_no: str, transport_id: Optional[str]
|
||
) -> bool:
|
||
if not transport_id:
|
||
return False
|
||
return (
|
||
db.query(PlFreightSettlementTransport.id)
|
||
.join(
|
||
PlFreightSettlement,
|
||
PlFreightSettlement.id == PlFreightSettlementTransport.settlement_id,
|
||
)
|
||
.filter(
|
||
PlFreightSettlementTransport.transport_id == transport_id,
|
||
PlFreightSettlementTransport.tenant_no == tenant_no,
|
||
PlFreightSettlementTransport.del_flag == 0,
|
||
PlFreightSettlement.tenant_no == tenant_no,
|
||
PlFreightSettlement.del_flag == 0,
|
||
PlFreightSettlement.settlement_status.in_(_LOCKED_SETTLEMENT_STATUSES),
|
||
)
|
||
.first()
|
||
is not None
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 3:** 追加 `apply_archive_product_quantities`:
|
||
|
||
```python
|
||
def apply_archive_product_quantities(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
plan: PlPlan,
|
||
items: list[PlanArchiveProductItem],
|
||
user_id: str,
|
||
) -> None:
|
||
product_map = {
|
||
row.id: row
|
||
for row in db.query(PlPlanProduct).filter(
|
||
PlPlanProduct.plan_id == plan.id,
|
||
PlPlanProduct.tenant_no == tenant_no,
|
||
PlPlanProduct.del_flag == 0,
|
||
).all()
|
||
}
|
||
for item in items:
|
||
prod = product_map.get(item.id)
|
||
if not prod:
|
||
raise HTTPException(status_code=400, detail=f"产品明细不存在: {item.id}")
|
||
actual = Decimal(str(item.actual_qty))
|
||
prod.actual_qty = actual
|
||
price_in = prod.inbound_price or prod.tax_price
|
||
price_out = prod.outbound_price or prod.tax_price
|
||
if plan.plan_type == "inbound":
|
||
prod.inbound_qty = actual
|
||
if price_in is not None:
|
||
prod.inbound_amount = actual * Decimal(str(price_in))
|
||
elif plan.plan_type == "outbound":
|
||
prod.outbound_qty = actual
|
||
if price_out is not None:
|
||
prod.outbound_amount = actual * Decimal(str(price_out))
|
||
prod.update_by = user_id
|
||
```
|
||
|
||
- [ ] **Step 4:** 追加 `apply_archive_freight`:
|
||
|
||
```python
|
||
def apply_archive_freight(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
plan: PlPlan,
|
||
freight: PlanArchiveFreightBlock,
|
||
user_id: str,
|
||
) -> None:
|
||
if int(plan.has_freight or 0) != 1:
|
||
return
|
||
if not plan.transport_id:
|
||
raise HTTPException(status_code=400, detail="计划有运输费用但未安排运输,请先安排运输")
|
||
if transport_has_locked_freight_settlement(db, tenant_no, plan.transport_id):
|
||
raise HTTPException(status_code=400, detail="运输单已纳入运费结算,无法修改运费")
|
||
|
||
transport = (
|
||
db.query(PlTransport)
|
||
.filter(
|
||
PlTransport.id == plan.transport_id,
|
||
PlTransport.tenant_no == tenant_no,
|
||
PlTransport.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not transport:
|
||
raise HTTPException(status_code=400, detail="关联运输单不存在")
|
||
|
||
transport.distance_km = freight.distance_km
|
||
transport.freight_type = freight.freight_type or "by_weight"
|
||
transport.freight_price = freight.freight_price
|
||
amount = freight.freight_amount or Decimal("0")
|
||
transport.freight_amount = amount
|
||
if freight.transport_qty is not None:
|
||
transport.transport_qty = freight.transport_qty
|
||
if freight.has_invoice is not None:
|
||
transport.has_invoice = freight.has_invoice
|
||
transport.update_by = user_id
|
||
|
||
plan.freight_amount = amount
|
||
plan.update_by = user_id
|
||
```
|
||
|
||
- [ ] **Step 5:** 追加 `record_plan_change`:
|
||
|
||
```python
|
||
def record_plan_change(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
plan_id: str,
|
||
change_type: str,
|
||
change_content: str,
|
||
user_id: str,
|
||
user_name: Optional[str],
|
||
dept_id: str,
|
||
) -> None:
|
||
db.add(
|
||
PlPlanChange(
|
||
id=generate_uuid(),
|
||
tenant_no=tenant_no,
|
||
plan_id=plan_id,
|
||
change_type=change_type,
|
||
change_content=change_content,
|
||
change_user_id=user_id,
|
||
change_user_name=user_name,
|
||
create_by=user_id,
|
||
create_dept_id=dept_id,
|
||
)
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 6:** 验证 import
|
||
|
||
Run: `cd backend && python -c "from app.services.pl_plan_service import apply_archive_product_quantities; print('ok')"`
|
||
Expected: `ok`
|
||
|
||
---
|
||
|
||
### Task 3: 增强 `archive_plan` + 放宽 `update_plan`
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/api/pl/plan.py`
|
||
|
||
- [ ] **Step 1:** 扩展 import(在文件顶部 `from fastapi import ...` 增加 `Body`):
|
||
|
||
```python
|
||
from app.schemas.pl.plan import PlanArchiveBody, PlanWithProducts
|
||
from app.services.pl_plan_service import (
|
||
apply_archive_freight,
|
||
apply_archive_product_quantities,
|
||
record_plan_change,
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 2:** 在 `update_plan` 中**删除**以下阻断(约 L589–591):
|
||
|
||
```python
|
||
if plan.archive_status == "archived":
|
||
raise HTTPException(status_code=400, detail="已归档的计划不能修改")
|
||
```
|
||
|
||
- [ ] **Step 3:** 替换 `archive_plan` 为:
|
||
|
||
```python
|
||
@router.post("/{id}/archive", response_model=PlanWithProducts)
|
||
def archive_plan(
|
||
id: str,
|
||
payload: Optional[PlanArchiveBody] = Body(default=None),
|
||
db: Session = Depends(get_db),
|
||
user: DbUserContext = Depends(get_db_current_user),
|
||
) -> PlanWithProducts:
|
||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||
plan = _get_plan(db, tenant_no, id)
|
||
|
||
if plan.follow_status == "cancelled":
|
||
raise HTTPException(status_code=400, detail="已作废的计划不能归档")
|
||
|
||
already_archived = plan.archive_status == "archived"
|
||
|
||
# 无 body:向后兼容,仅首次改状态
|
||
if payload is None:
|
||
if already_archived:
|
||
raise HTTPException(status_code=400, detail="计划已归档,请提交归档表单以调整内容")
|
||
plan.archive_status = "archived"
|
||
plan.complete_status = "yes"
|
||
plan.follow_status = "completed"
|
||
plan.update_by = user.id
|
||
db.commit()
|
||
return get_plan(id, db, user)
|
||
|
||
apply_archive_product_quantities(
|
||
db, tenant_no=tenant_no, plan=plan, items=payload.products, user_id=user.id
|
||
)
|
||
if payload.freight is not None:
|
||
apply_archive_freight(
|
||
db, tenant_no=tenant_no, plan=plan, freight=payload.freight, user_id=user.id
|
||
)
|
||
elif int(plan.has_freight or 0) == 1 and not already_archived:
|
||
if not plan.transport_id:
|
||
raise HTTPException(status_code=400, detail="计划有运输费用但未安排运输,请先安排运输")
|
||
|
||
if not already_archived:
|
||
plan.archive_status = "archived"
|
||
plan.complete_status = "yes"
|
||
plan.follow_status = "completed"
|
||
|
||
plan.update_by = user.id
|
||
record_plan_change(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
plan_id=plan.id,
|
||
change_type="archive_adjust" if already_archived else "archive",
|
||
change_content="调整归档内容" if already_archived else "运输计划归档",
|
||
user_id=user.id,
|
||
user_name=user.real_name or user.username,
|
||
dept_id=user.dept_id,
|
||
)
|
||
db.commit()
|
||
return get_plan(id, db, user)
|
||
```
|
||
|
||
- [ ] **Step 4:** 确认 `get_plan` 在同文件已存在且返回 `PlanWithProducts`(复用现有 GET 逻辑)
|
||
|
||
- [ ] **Step 5:** 验证路由加载
|
||
|
||
Run: `cd backend && python -c "from app.api.pl.plan import archive_plan; print('ok')"`
|
||
Expected: `ok`
|
||
|
||
---
|
||
|
||
### Task 4: 前端 API 类型
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/api/pl/index.ts`
|
||
|
||
- [ ] **Step 1:** 追加类型:
|
||
|
||
```typescript
|
||
export interface PlanArchiveProductItem {
|
||
id: string
|
||
actualQty: number
|
||
}
|
||
|
||
export interface PlanArchiveFreightBlock {
|
||
distanceKm?: number
|
||
freightType?: string
|
||
freightPrice?: number
|
||
freightAmount?: number
|
||
transportQty?: number
|
||
hasInvoice?: number
|
||
}
|
||
|
||
export interface PlanArchiveBody {
|
||
products: PlanArchiveProductItem[]
|
||
freight?: PlanArchiveFreightBlock
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2:** 扩展 `PlanQuery`(若尚未有):
|
||
|
||
```typescript
|
||
archiveStatus?: string
|
||
```
|
||
|
||
- [ ] **Step 3:** 修改 `planApi.archive`:
|
||
|
||
```typescript
|
||
archive: (id: string, data?: PlanArchiveBody) =>
|
||
request.post<Plan>(`/transport/plan/${id}/archive`, data),
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: 产品表 `editableActualQtyOnly`
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue`
|
||
|
||
- [ ] **Step 1:** props 增加:
|
||
|
||
```typescript
|
||
/** 仅实际数量可编辑,计划数量只读(归档弹窗) */
|
||
editableActualQtyOnly?: boolean
|
||
```
|
||
|
||
- [ ] **Step 2:** 计划数量列 template 改为:
|
||
|
||
```vue
|
||
<el-input-number
|
||
v-if="editableQty && !editableActualQtyOnly"
|
||
v-model="row.quantity"
|
||
...
|
||
/>
|
||
```
|
||
|
||
- [ ] **Step 3:** 实际数量列 template 改为:
|
||
|
||
```vue
|
||
<el-input-number
|
||
v-if="(editableQty && showActualQty) || editableActualQtyOnly"
|
||
v-model="row.actualQty"
|
||
...
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: `PlPlanArchiveDialog`
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/components/business/pl-plan-archive-dialog/index.vue`
|
||
- Reference: `web-admin/src/components/business/pl-plan-arrange-dialog/index.vue`(布局 SCSS 类 `pl-arrange-layout` 等可复用)
|
||
|
||
- [ ] **Step 1:** 创建弹窗骨架(props: `visible`, `planId`;emit: `update:visible`, `success`)
|
||
|
||
- [ ] **Step 2:** 上区表单:
|
||
- `PlPlanFormProductTable`:`show-actual-qty` + `editable-actual-qty-only`
|
||
- 运费区块:从 arrange-dialog 复制 L189–236 结构,绑定 `formData` + `freightCalc`
|
||
- `usePlFreightCalc({ products: productRows, hasFreight, freightType })`
|
||
|
||
- [ ] **Step 3:** 下区:`PlPlanViewTabs ref="tabsRef" :plan-id="planId" embedded`
|
||
|
||
- [ ] **Step 4:** 计算属性:
|
||
|
||
```typescript
|
||
const isAdjustMode = computed(() => plan.value.archiveStatus === 'archived')
|
||
const dialogTitle = computed(() => {
|
||
const no = plan.value.planNo || ''
|
||
return isAdjustMode.value
|
||
? no ? `调整归档内容 · ${no}` : '调整归档内容'
|
||
: no ? `运输计划归档 · ${no}` : '运输计划归档'
|
||
})
|
||
const submitLabel = computed(() => (isAdjustMode.value ? '确认保存' : '确认归档'))
|
||
```
|
||
|
||
- [ ] **Step 5:** `handleOpened`:加载 `planApi.get(planId)`,映射 products → `productRows`,从 plan/transport 预填运费字段
|
||
|
||
- [ ] **Step 6:** `handleSubmit`:
|
||
|
||
```typescript
|
||
const body: PlanArchiveBody = {
|
||
products: productRows.value.map(row => ({
|
||
id: row.productId, // 注意:用 pl_plan_product.id,不是 CRM productId
|
||
actualQty: Number(row.actualQty ?? 0)
|
||
})),
|
||
freight: formData.hasFreight === 1 ? {
|
||
distanceKm: formData.distanceKm ?? undefined,
|
||
freightType: formData.freightType,
|
||
freightPrice: freightCalc.freightPrice.value ?? undefined,
|
||
freightAmount: freightCalc.freightAmount.value ?? undefined,
|
||
transportQty: freightCalc.transportQty.value ?? undefined,
|
||
hasInvoice: formData.hasInvoice
|
||
} : undefined
|
||
}
|
||
await planApi.archive(props.planId, body)
|
||
ElMessage.success(isAdjustMode.value ? '保存成功' : '归档成功')
|
||
tabsRef.value?.reload?.()
|
||
emit('success')
|
||
innerVisible.value = false
|
||
```
|
||
|
||
> **注意:** `productRows` 映射时必须保留 `pl_plan_product.id`(从 API `PlanProduct.id`),勿与 `productId` 混淆。
|
||
|
||
- [ ] **Step 7:** 样式:复用 arrange-dialog 的 `pl-arrange-layout` / `pl-arrange-form-panel` / `pl-arrange-view-panel`(可在 scoped 中 `@use` 或复制最小 SCSS)
|
||
|
||
---
|
||
|
||
### Task 7: `usePlPlanList` composable
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/composables/usePlPlanList.ts`
|
||
- Modify: `web-admin/src/views/pl/plan/index.vue`(可选,YAGNI:若时间紧可只在 plan-archive 复制,后续再抽)
|
||
|
||
- [ ] **Step 1:** 抽取共享状态与方法:
|
||
|
||
```typescript
|
||
export function usePlPlanList(options?: { extraSearchFields?: () => Record<string, unknown> }) {
|
||
const loading = ref(false)
|
||
const planList = ref<Plan[]>([])
|
||
const selectedRows = ref<Plan[]>([])
|
||
const dateRange = ref<string[]>([])
|
||
const filterExpanded = ref(false)
|
||
const searchForm = reactive({
|
||
planNo: '',
|
||
planType: '',
|
||
followStatus: '',
|
||
transportStatus: '',
|
||
customerName: '',
|
||
archiveStatus: '' // 归档页使用
|
||
})
|
||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||
|
||
async function fetchData() { /* planApi.list 参数组装 */ }
|
||
function handleSearch() { pagination.page = 1; fetchData() }
|
||
function handleReset() { /* 清空 + fetchData */ }
|
||
// ... handleSelectionChange, handlePageChange, handleSizeChange
|
||
|
||
return { loading, planList, selectedRows, dateRange, filterExpanded, searchForm, pagination, fetchData, handleSearch, handleReset, ... }
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2:** (可选)`plan/index.vue` 改用 composable,确保行为不变
|
||
|
||
---
|
||
|
||
### Task 8: 归档列表页
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/views/pl/plan-archive/index.vue`
|
||
|
||
- [ ] **Step 1:** 复制 `plan/index.vue` 筛选区 + 表格列,标题改为 `运输计划归档`
|
||
|
||
- [ ] **Step 2:** 展开区增加「归档状态」:
|
||
|
||
```vue
|
||
<el-form-item label="归档状态">
|
||
<el-select v-model="searchForm.archiveStatus" placeholder="全部" clearable>
|
||
<el-option label="未归档" value="unarchived" />
|
||
<el-option label="已归档" value="archived" />
|
||
</el-select>
|
||
</el-form-item>
|
||
```
|
||
|
||
- [ ] **Step 3:** 工具栏:保留「已选 N 项」;**不放置「批量归档」**(归档必须经弹窗填写 actualQty/运费,无法无表单批量)
|
||
|
||
- [ ] **Step 4:** 操作列(`min-width="260"`):
|
||
|
||
```vue
|
||
<el-button link type="primary" @click="handleView(row)">
|
||
<el-icon><View /></el-icon>查看
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.followStatus !== 'cancelled'"
|
||
link type="primary"
|
||
@click="handleEdit(row)"
|
||
>
|
||
<el-icon><Edit /></el-icon>编辑
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.followStatus !== 'cancelled'"
|
||
link type="success"
|
||
@click="handleArchive(row)"
|
||
>
|
||
<el-icon><FolderChecked /></el-icon>{{ row.archiveStatus === 'archived' ? '调整' : '归档' }}
|
||
</el-button>
|
||
```
|
||
|
||
- [ ] **Step 5:** 挂载 `PlPlanViewDialog`、`PlPlanFormDialog`、`PlPlanArchiveDialog`
|
||
|
||
- [ ] **Step 6:** `handleBatchArchive`:**不实现**(见 Step 3)
|
||
|
||
- [ ] **Step 7:** `onMounted` → `fetchData()`
|
||
|
||
---
|
||
|
||
### Task 9: 路由、菜单、标题竖条
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/router/modules/pl.ts`
|
||
- Modify: `docs/sql/shanxia2.0.sql`
|
||
- Modify: `web-admin/src/utils/router/menu-icon.ts`
|
||
- Modify: `web-admin/src/App.vue`(若存在 `panelTitleBarTextRules`)或确认 `app.scss` 对 `运输计划归档` 标题生效
|
||
|
||
- [ ] **Step 1:** `pl.ts` 在 `PlPlan` 后插入:
|
||
|
||
```typescript
|
||
{
|
||
id: 'PL00000000000000000000000000016',
|
||
name: 'PlPlanArchive',
|
||
path: 'plan-archive',
|
||
component: '/pl/plan-archive/index',
|
||
meta: { title: '运输计划归档', keepAlive: true, menuCode: 'pl:plan-archive:list' }
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2:** `shanxia2.0.sql` 追加菜单与按钮(`order_num=12`,危化品证上传改为 13):
|
||
|
||
```sql
|
||
INSERT INTO `sys_menu` (...) VALUES
|
||
('PL_PLAN_ARCHIVE', '运输计划归档', 'PL_MENU_ROOT', 'C', 'plan-archive', 'pl/plan-archive/index', 'pl:plan-archive:list', 1, '1', '1', 'shanxia2.0', 12, NULL, 'system', '', NOW(), NULL, NOW(), 0);
|
||
|
||
INSERT INTO `sys_menu` (...) VALUES
|
||
('PL_PLAN_ARCHIVE_Q', '查询', 'PL_PLAN_ARCHIVE', 'F', '', '', 'pl:plan-archive:query', 1, '1', '1', 'shanxia2.0', 1, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||
('PL_PLAN_ARCHIVE_E', '编辑', 'PL_PLAN_ARCHIVE', 'F', '', '', 'pl:plan-archive:edit', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||
('PL_PLAN_ARCHIVE_A', '归档', 'PL_PLAN_ARCHIVE', 'F', '', '', 'pl:plan-archive:archive', 1, '1', '1', 'shanxia2.0', 3, NULL, 'system', '', NOW(), NULL, NOW(), 0);
|
||
```
|
||
|
||
- [ ] **Step 3:** `menu-icon.ts`:
|
||
|
||
```typescript
|
||
'pl:plan-archive:list': 'ri:folder-check-line',
|
||
```
|
||
|
||
- [ ] **Step 4:** 页面 `h2.title-bar-heading` 文案为 **运输计划归档**(与规范竖条一致)
|
||
|
||
---
|
||
|
||
### Task 10: 原运输计划页移除归档
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/pl/plan/index.vue`
|
||
- Modify: `web-admin/src/views/pl/plan/detail.vue`
|
||
|
||
- [ ] **Step 1:** `plan/index.vue` 删除工具栏「批量归档」按钮、`handleBatchArchive`、`FolderChecked` import(无其他引用时)
|
||
|
||
- [ ] **Step 2:** `plan/detail.vue` 删除「归档」按钮、`handleArchive` 函数、`FolderChecked` import(无其他引用时)
|
||
|
||
---
|
||
|
||
### Task 11: 联调验收
|
||
|
||
- [ ] **V1** 打开 `/#/pl/plan-archive` — 列表与筛选正常,含归档状态筛选项
|
||
- [ ] **V2** 未归档行 — 查看 / 编辑 / 归档可用
|
||
- [ ] **V3** 已归档行 — 查看 / 编辑 / 调整可用
|
||
- [ ] **V5** 首次归档 — actualQty、运费、状态变为 archived/completed
|
||
- [ ] **V6** 已归档调整 — 仅数据更新,状态保持 archived
|
||
- [ ] **V6a** 已归档编辑 — `PlPlanFormDialog` 保存成功
|
||
- [ ] **V7** `/#/pl/plan` 无批量归档;详情无归档按钮
|
||
- [ ] **V9** 有运费未安排运输 — 归档 400
|
||
- [ ] **V10** 按重量 — actualQty 变更联动运输量
|
||
- [ ] **V11** 下区司机/现场链接可复制
|
||
|
||
Run frontend lint(改动文件):
|
||
|
||
```bash
|
||
cd web-admin && pnpm lint
|
||
```
|
||
|
||
Run backend import check:
|
||
|
||
```bash
|
||
cd backend && python -c "from app.main import app; print('ok')"
|
||
```
|
||
|
||
Run graphify update:
|
||
|
||
```bash
|
||
cd /Users/zlwu/work/00code/shanxia2.0 && ./scripts/update-graph.sh all
|
||
```
|
||
|
||
---
|
||
|
||
## Spec 覆盖自检
|
||
|
||
| Spec 条目 | Task |
|
||
|-----------|------|
|
||
| D1 全部计划列表 | Task 8 |
|
||
| D2 已归档可调整 | Task 3, 6, 8 |
|
||
| D2a 已归档可编辑 | Task 3, 8 |
|
||
| D3 原页移除归档 | Task 10 |
|
||
| D4 弹窗布局 | Task 6 |
|
||
| D5 增强 API | Task 1–3 |
|
||
| D6 筛选一致 + 归档状态 | Task 7–8 |
|
||
| V1–V12 验收 | Task 11 |
|
||
|
||
---
|
||
|
||
## 执行顺序建议
|
||
|
||
1. Task 1 → 2 → 3(后端可先联调 Swagger)
|
||
2. Task 4 → 5 → 6(归档弹窗)
|
||
3. Task 7 → 8 → 9(列表 + 路由菜单)
|
||
4. Task 10 → 11(清理 + 验收)
|