This commit is contained in:
zlwu committed 2026-08-06 09:58:20 +08:00
1 parent 2716c2b6f8
commit f3693be913
140 files changed
+799199 -11262

No files matched your search

@@ -0,0 +1,128 @@
# 合同审批中/已审批可再编辑回草稿 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:** 允许 `approving` / `approved`(未执行)合同再次编辑;保存后变为 `draft` 并清理进行中审批,可再次提交审批。
**Architecture:** 后端放宽 `update_contract` 状态门禁,保存时统一回草稿并按需 `delete_pending_approvals`。前端抽出可编辑状态与确认文案工具函数,列表/详情/查看弹窗打开编辑前对非草稿可编辑状态弹确认。
**Tech Stack:** Python 3.9 / FastAPI CRUD、pytest、Vue 3 / Element Plus
**Spec:** `docs/superpowers/specs/2026-08-05-crm-contract-reedit-to-draft-design.md`
---
## File map
| 文件 | 职责 |
|------|------|
| `backend/app/crud/crm/contract.py` | `update_contract`:可编辑状态校验 + 回草稿副作用 |
| `backend/tests/test_crm_contract_reedit_to_draft.py` | 门禁断言单测 |
| `web-admin/src/views/crm/contract/contractType.ts` | `isContractEditableStatus`、`getContractReeditConfirmMessage` |
| `web-admin/src/views/crm/contract/index.vue` | 列表编辑按钮与 `handleUpdate` |
| `web-admin/src/views/crm/contract/detail.vue` | 详情「修改」按钮与 `handleEdit` |
| `web-admin/src/views/crm/contract/components/ContractDetailDialog.vue` | 查看弹窗「修改」 |
---
### Task 1: 后端 `update_contract` 放宽门禁 + 回草稿
**Files:**
- Modify: `backend/app/crud/crm/contract.py`(`update_contract`)
- Create: `backend/tests/test_crm_contract_reedit_to_draft.py`
- [x] **Step 1: 增加可编辑状态常量与断言函数(紧挨 `update_contract` 之前)**
- [x] **Step 2: 改写 `update_contract` 门禁与回草稿副作用**
- [x] **Step 3: 写单测**
- [x] **Step 4: 运行单测**
---
### Task 2: 前端共享工具函数
**Files:**
- Modify: `web-admin/src/views/crm/contract/contractType.ts`
- [ ] **Step 1: 追加导出**
```typescript
export const CONTRACT_EDITABLE_STATUSES = ['draft', 'approving', 'approved'] as const
export type ContractEditableStatus = (typeof CONTRACT_EDITABLE_STATUSES)[number]
export function isContractEditableStatus(status?: string | null): boolean {
return CONTRACT_EDITABLE_STATUSES.includes(
(status || '').trim() as ContractEditableStatus
)
}
export function getContractReeditConfirmMessage(status?: string | null): string | null {
const s = (status || '').trim()
if (s === 'approving') {
return '编辑保存后将作废当前审批流,合同变回草稿,需重新提交审批'
}
if (s === 'approved') {
return '编辑保存后合同将变回草稿,需重新提交审批'
}
return null
}
```
---
### Task 3: 合同列表 `index.vue`
**Files:**
- Modify: `web-admin/src/views/crm/contract/index.vue`
- [ ] **Step 1: import** `isContractEditableStatus`、`getContractReeditConfirmMessage` from `./contractType`
- [ ] **Step 2: 编辑按钮** `:disabled="single || !isContractEditableStatus(selectedRows[0]?.status)"`
- [ ] **Step 3: `handleUpdate` 改为 async:可编辑校验 → 有确认文案则 `$modal.confirm` → 打开表单弹窗**
文案失败提示:`仅草稿、审批中或已审批(未执行)的合同才能修改`
---
### Task 4: 详情页 `detail.vue`
**Files:**
- Modify: `web-admin/src/views/crm/contract/detail.vue`
- [ ] **Step 1: 「修改」按钮** `v-if="isContractEditableStatus(contract?.status)"`
- [ ] **Step 2: `handleEdit` 同样:校验 → 确认 → `router.push` 表单页
---
### Task 5: 查看弹窗 `ContractDetailDialog.vue`
**Files:**
- Modify: `web-admin/src/views/crm/contract/components/ContractDetailDialog.vue`
- [ ] **Step 1: 「修改」按钮 v-if 使用 `isContractEditableStatus`**
- [ ] **Step 2: `handleDetailEdit`:校验 → 确认 → 关闭详情并打开编辑弹窗**
---
### Task 6: 验收与图谱
- [ ] 再跑 `pytest tests/test_crm_contract_reedit_to_draft.py -v`
- [ ] 对照 spec 验收要点手工核对
- [ ] `./scripts/update-graph.sh backend` 与 `./scripts/update-graph.sh frontend`
---
## Spec coverage
| Spec 要求 | Task |
|-----------|------|
| 可编辑 draft/approving/approved | 1, 2 |
| 不开放 executing/finished/invalid | 1, 3–5 |
| 打开编辑前确认文案 | 2–5 |
| 保存回 draft + 清 approval_status | 1 |
| approving 清理待审 | 1 |
| 提交审批仍仅 draft | 不变 |
| 列表/详情/弹窗入口 | 3–5 |
@@ -0,0 +1,44 @@
# 地址库选择:编辑 + 联系方式回填 + 简易检索 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:** 改动集中在选择弹窗与 `PlAddressWithLibrary`;父表单通过可选 `v-model:contact` 接收回填。后端 list `q` 已覆盖名称/地址/联系人/电话,无需改 API。
**Tech Stack:** Vue 3 + Element Plus + 现有 `transportAddressApi`
**Spec:** `docs/superpowers/specs/2026-08-06-pl-address-select-edit-and-contact-fill-design.md`
---
### Task 1: 选择弹窗 — 检索 / 列表 / 编辑
**Files:**
- Modify: `web-admin/src/components/business/pl-transport-address-select-dialog/index.vue`
- [x] 移除顶部 `filter-block`;工具栏增加简易 `el-input`(回车/清空检索)+「新增地址」
- [x] 去掉「坐标」列;操作列增加「编辑」,`min-width` ≥ 160
- [x] 嵌套表单支持新增/编辑:`editingId`、标题切换、`update` 成功后刷新列表不 select
- [x] 新增成功仍 `handleConfirm`
### Task 2: 地址组件联系方式回填
**Files:**
- Modify: `web-admin/src/components/business/pl-address-with-library/index.vue`
- [x] 增加可选 `contact` prop + `update:contact`
- [x] `handleLibrarySelect`:有联系信息时 emit `姓名 电话`(或单侧)
### Task 3: 父表单绑定
**Files:**
- Modify: `web-admin/src/components/business/pl-plan-form-dialog/index.vue`
- Modify: `web-admin/src/components/business/pl-plan-arrange-dialog/index.vue`
- [x] 取货/送货 `PlAddressWithLibrary` 增加 `v-model:contact` 绑定对应联系方式字段
### Task 4: 收尾
- [x] `./scripts/update-graph.sh frontend`(或 `graphify update .`)
- [ ] 手工核对:检索、编辑、选择回填、空联系人不覆盖
@@ -0,0 +1,44 @@
# 运输计划「从承运商库选择」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:** 运输计划弹窗「运输公司」旁提供「从承运商库选择」,支持搜索/选择/新增/编辑并回填 `carrierId`。
**Architecture:** 对齐车辆库/地址库:`PlCarrierLibrarySelectDialog` + 嵌套 `PlCarrierFormDialog`(对照 `views/pl/carrier`);计划表单旁按钮接入。
**Tech Stack:** Vue 3 + Element Plus + `carrierApi`
**Spec:** `docs/superpowers/specs/2026-08-06-pl-plan-carrier-library-select-design.md`
---
## 文件结构
| 操作 | 文件 |
|------|------|
| Create | `web-admin/src/components/business/pl-carrier-form-dialog/index.vue` |
| Create | `web-admin/src/components/business/pl-carrier-library-select-dialog/index.vue` |
| Modify | `web-admin/src/components/business/pl-plan-form-dialog/index.vue` |
---
### Task 1: PlCarrierFormDialog
- [x] Props: `modelValue`, `carrierId?`;Emits: `update:modelValue`, `saved({ mode, carrier })`
- [x] 弹窗 960px,对照管理页完整表单 + 资质附件
- [x] create/update;create 返回详情 emit;edit 后 get 再 emit
### Task 2: PlCarrierLibrarySelectDialog
- [x] 1200px 选弹窗;搜索 + 列表 + 选择/编辑 + 新增
- [x] 新增成功 auto-select;编辑成功刷新列表
### Task 3: 接入计划表单
- [x] 运输公司旁「从承运商库选择」;`ensureSelectedCarrierOption` 回填
### Task 4: 收尾
- [x] 设计文档状态 → 已实现
- [x] `./scripts/update-graph.sh frontend`
- [ ] 浏览器走查(代码已就绪,需本地验证)
@@ -0,0 +1,30 @@
# 危化品证上传筛选 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:** 危化品证上传页只展示含易制毒/易制爆的计划,并新增「未上传证书」Tab。
**Architecture:** 在 `pl_plan_service` 提供 EXISTS 子查询过滤器;`list_plans` 暴露查询参数;本页 Tab 扩展并始终带上筛选参数。
**Tech Stack:** FastAPI + SQLAlchemy、Vue 3、pytest
---
## File map
| File | Responsibility |
|------|----------------|
| `backend/app/services/pl_plan_service.py` | `plan_has_hazard_product_exists` / `plan_has_incomplete_hazard_cert_exists` |
| `backend/app/api/pl/plan.py` | `list_plans` 接入新参数 |
| `backend/tests/test_pl_plan_cert_list_filters.py` | SQL/行为单测 |
| `web-admin/src/api/pl/index.ts` | `PlanQuery` 字段 |
| `web-admin/src/utils/pl-display.ts` | 本页 Tab 常量与映射 |
| `web-admin/src/components/business/pl-plan-type-tabs/index.vue` | 可选 `tabs` |
| `web-admin/src/views/pl/plan-cert/index.vue` | 请求参数与 Tab 行为 |
## Tasks
- [x] Task 1: 后端过滤器单测(红)
- [x] Task 2: 实现过滤器并接入 `list_plans`(绿)
- [x] Task 3: 前端 Tab + API 参数
- [x] Task 4: 跑测与 graphify 更新
@@ -0,0 +1,19 @@
# 运输计划取货/送货联系方式 Implementation Plan
> **For agentic workers:** Steps use checkbox syntax for tracking.
**Goal:** 在 `pl_plan` 增加取货/送货联系方式,并同步表单布局与各展示面。
**Architecture:** 仅计划表落库;出入库与公开页从计划合并/读取。
**Tech Stack:** FastAPI + SQLAlchemy + Vue 3 web-admin
---
## Tasks
- [x] DB migration + `docs/sql/shanxia2.0.sql` CREATE
- [x] Model / schemas / plan+transport+public APIs / merge service + unit test
- [x] Form dialog + arrange dialog layout & bind
- [x] View tabs + IoOrder view + public H5 + driver summary text
- [x] Frontend API types
@@ -0,0 +1,367 @@
# 运输计划「从车辆库选择」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:** 在运输计划新增/编辑弹窗车牌号旁提供「从车辆库选择」,弹窗分三方/自有 Tab,支持搜索、选择、新增、编辑,并回填计划车辆。
**Architecture:** 对齐 `PlTransportAddressSelectDialog`:计划表单旁按钮打开 `PlVehicleLibrarySelectDialog`(Tab + 列表);嵌套 `PlVehicleFormDialog` 对照 `views/pl/vehicle` / `views/pl/self-vehicle` 完整表单。选择或新增成功后写 `vehicleId` 并走现有 `handleVehicleChange`。首期不改造两管理页。
**Tech Stack:** Vue 3 + Element Plus + 现有 `vehicleApi` / `usePlRemoteVehicleOptions` / `PlVehicleCertPartTabs`
**Spec:** `docs/superpowers/specs/2026-08-06-pl-plan-vehicle-library-select-design.md`
---
## 文件结构
| 操作 | 文件 | 职责 |
|------|------|------|
| Create | `web-admin/src/components/business/pl-vehicle-form-dialog/index.vue` | 新增/编辑车辆完整表单(`vehicleType: 'carrier' \| 'self'`) |
| Create | `web-admin/src/components/business/pl-vehicle-library-select-dialog/index.vue` | 选车大弹窗:Tab + 搜索 + 列表 + 挂载表单弹窗 |
| Modify | `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | 车牌旁按钮 + 选车回填 |
参考(只读对照,勿在首期强行抽离改造):
- `web-admin/src/components/business/pl-transport-address-select-dialog/index.vue`
- `web-admin/src/components/business/pl-address-with-library/index.vue`
- `web-admin/src/views/pl/vehicle/index.vue`(三方表单弹窗)
- `web-admin/src/views/pl/self-vehicle/index.vue`(自有表单弹窗)
---
### Task 1: `PlVehicleFormDialog` — 完整新增/编辑表单
**Files:**
- Create: `web-admin/src/components/business/pl-vehicle-form-dialog/index.vue`
- [x] **Step 1: 建立组件壳与 API**
Props / 事件约定:
```ts
const props = defineProps<{
modelValue: boolean
/** carrier=三方,self=自有;锁定类型,不可切换 */
vehicleType: 'carrier' | 'self'
/** 有值则为编辑,无值则为新增 */
vehicleId?: string | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
/** 新增成功:返回详情;编辑成功:返回详情(父级刷新列表用) */
saved: [payload: { mode: 'create' | 'edit'; vehicle: VehicleDetail }]
}>()
```
弹窗:
- `width="1360px"`,`class="dialog-form-layout"`,`append-to-body`,`destroy-on-close`
- `modal-class="order-module-dialog-mask"`,`style="backdrop-filter: none"`
- 标题:
- create + carrier → `新增车辆`
- create + self → `新增自有车辆`
- edit + carrier → `编辑车辆`
- edit + self → `编辑自有车辆`
- [x] **Step 2: 移植表单 UI 与状态**
从 `views/pl/vehicle/index.vue` / `self-vehicle/index.vue` 的 `el-dialog` 表单区对照实现:
1. 基本信息 `form-grid-3`:车牌号、挂车号、载重、承运商、车辆状态、备注、车辆照片(`AttachmentUploadField`,`upload-folder="pl/vehicle-photo"`)
2. 关联人员子表(添加人员 / 角色 driver|escort / 删除)
3. `PlVehicleCertPartTabs`:
- `vehicleType === 'self'`:`enforce-self-vehicle-required`(与自有页一致)
- `vehicleType === 'carrier'`:`:enforce-self-vehicle-required="false"` 且 `:enforce-carrier-vehicle-required="false"`(与三方页一致)
人员远程选项:
```ts
usePlRemotePersonnelOptions({
listParams: () => ({
status: '1',
personnelAffiliation: props.vehicleType === 'carrier' ? 'carrier' : 'self'
})
})
```
证书槽初始化:
- carrier:`ensureVehicleCertSlots(..., trailerNo, 'carrier')`
- self:`ensureSelfVehicleCertSlots(..., trailerNo)`
校验:复用管理页 `validateSubTables` 逻辑(人员必选、角色、`validatePersonnelTypeRoleCompat`、`validatePersonnelAffiliationVehicleCompat`)。
- [x] **Step 3: open / submit**
`@open` 或 `watch(modelValue)`:
- 新增:重置表单;`vehicleStatus='available'`;证书空槽;加载人员选项
- 编辑:`vehicleApi.get(vehicleId)` 回填;编辑自有时若 `detail.vehicleType !== 'self'` 则警告并关闭;`ensureSelectedCarrierOption`;`nextTick` 后再填证书并 `refreshRequiredSlots`
提交 payload 与管理页一致(含 `personnelList` / `certList`);`vehicleType` 固定为 prop。
```ts
if (isEdit) {
await vehicleApi.update(formData.id, payload)
const res = await vehicleApi.get(formData.id)
const detail = pickBxData<VehicleDetail>(res)!
emit('saved', { mode: 'edit', vehicle: detail })
} else {
const res = await vehicleApi.create(payload)
let detail = pickBxData<VehicleDetail>(res)
if (!detail?.id) {
// 若 create 未回详情,不应发生;兜底可再 get —— 优先用返回体
ElMessage.error('新增成功但未返回车辆信息')
return
}
emit('saved', { mode: 'create', vehicle: detail })
}
```
成功后关闭弹窗;上传中拦截与管理页相同文案。
- [ ] **Step 4: 手工冒烟(可选先挂临时入口)**
在浏览器临时打开组件或等 Task 2 挂载后验证:三方/自有各新增一条、各编辑一条,证书与人员校验行为与管理页一致。
- [ ] **Step 5: Commit(若用户要求提交时再执行)**
```bash
git add web-admin/src/components/business/pl-vehicle-form-dialog/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add PlVehicleFormDialog for carrier/self vehicle CRUD
EOF
)"
```
---
### Task 2: `PlVehicleLibrarySelectDialog` — Tab 选车弹窗
**Files:**
- Create: `web-admin/src/components/business/pl-vehicle-library-select-dialog/index.vue`
- [x] **Step 1: 弹窗外壳(对齐地址库选择)**
```vue
<el-dialog
v-model="innerVisible"
title="从车辆库选择"
width="1360px"
align-center
destroy-on-close
append-to-body
class="dialog-form-layout pl-vehicle-library-select-dialog"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
style="backdrop-filter: none"
@open="handleOpen"
>
```
结构:
1. `el-tabs`:`carrier` / `self`(文案:三方车辆 / 自有车辆)
2. 工具栏:`el-input` 搜索(placeholder:`搜索车牌号`,回车/清空检索)+ `type="primary"`「新增车辆」
3. `ListTable` + `ListPagination`(`dialog-list-body-only` 风格参考地址库弹窗)
4. 嵌套 `<PlVehicleFormDialog v-model="formVisible" :vehicle-type="activeTab" :vehicle-id="editingId" @saved="handleFormSaved" />`
- [x] **Step 2: 列表与选择**
```ts
async function fetchList() {
loading.value = true
try {
const res = await vehicleApi.list({
vehicleType: activeTab.value, // 'carrier' | 'self'
q: keyword.value.trim() || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
const { list, total } = pickBxPageResult<Vehicle>(res)
rows.value = list
pagination.total = total
} finally {
loading.value = false
}
}
```
列:序号、车牌号(`el-link` → `handleConfirm`)、挂车号、车辆状态 Tag、载重、承运商(`v-if="activeTab === 'carrier'"`)、关联人员摘要、操作(编辑 / 选择)。
```ts
function handleConfirm(row: Vehicle) {
emit('select', row)
innerVisible.value = false
}
function handleFormSaved(payload: { mode: 'create' | 'edit'; vehicle: VehicleDetail }) {
formVisible.value = false
editingId.value = null
if (payload.mode === 'create') {
handleConfirm(payload.vehicle)
return
}
fetchList()
}
```
样式:`:deep(.el-dialog__body) { min-height: 560px; }`;表格 `:max-height="560"`。
- [x] **Step 3: Tab / 打开重置**
`handleOpen`:keyword 清空、page=1、activeTab 默认 `carrier`、关表单、`fetchList`。
Tab `@tab-change`:page=1 再 `fetchList`。
- [ ] **Step 4: Commit(若用户要求)**
```bash
git add web-admin/src/components/business/pl-vehicle-library-select-dialog/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add vehicle library select dialog with carrier/self tabs
EOF
)"
```
---
### Task 3: 接入运输计划表单弹窗
**Files:**
- Modify: `web-admin/src/components/business/pl-plan-form-dialog/index.vue`
- [x] **Step 1: 车牌号旁按钮布局**
将原「车牌号」单项改为与地址库相同的行布局:
```vue
<el-form-item label="车牌号">
<div class="pl-plan-vehicle-with-library">
<el-select
v-model="formData.vehicleId"
class="pl-plan-vehicle-with-library__select"
placeholder="请选择车牌号"
filterable
remote
clearable
:remote-method="searchVehicles"
:loading="vehicleSelectLoading || vehicleLoading"
@focus="onVehicleSelectFocus"
@change="handleVehicleChange"
>
<el-option
v-for="item in vehicleList"
:key="item.id"
:label="item.licensePlate"
:value="item.id"
/>
</el-select>
<el-button
class="pl-plan-vehicle-with-library__btn btn-ghost"
@click="vehicleLibraryVisible = true"
>
<el-icon><FolderOpened /></el-icon>从车辆库选择
</el-button>
</div>
</el-form-item>
<PlVehicleLibrarySelectDialog
v-model="vehicleLibraryVisible"
@select="handleVehicleLibrarySelect"
/>
```
样式(scoped,对照 `pl-address-with-library`):
```scss
.pl-plan-vehicle-with-library {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.pl-plan-vehicle-with-library__select {
flex: 1 1 auto;
min-width: 0;
}
.pl-plan-vehicle-with-library__btn {
flex: 0 0 auto;
white-space: nowrap;
}
```
- [x] **Step 2: 回填处理**
```ts
const vehicleLibraryVisible = ref(false)
async function handleVehicleLibrarySelect(vehicle: Vehicle) {
if (!vehicle?.id) return
await ensureSelectedVehicleOption(vehicle.id)
formData.vehicleId = vehicle.id
await handleVehicleChange(vehicle.id)
if (vehicle.carrierId) {
await ensureSelectedCarrierOption(vehicle.carrierId)
}
}
```
导入:`FolderOpened`、`PlVehicleLibrarySelectDialog`、类型 `Vehicle`(若尚未导入)。
- [ ] **Step 3: Commit(若用户要求)**
```bash
git add web-admin/src/components/business/pl-plan-form-dialog/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): wire vehicle library select into plan form dialog
EOF
)"
```
---
### Task 4: 验收与图谱
- [ ] **Step 1: 手工验收清单**(代码已就绪,需在本地浏览器走查)
在 `/#/pl/plan-module/plan` 打开新增/编辑运输计划:
1. 车牌旁可见「从车辆库选择」,布局与「从地址库选择」同类
2. 弹窗 1360 宽、偏高;Tab 切换三方/自有列表正确过滤
3. 搜索车牌有效;选择 / 链接 / 双击可回填车牌,摘要条与承运商更新
4. 三方 Tab「新增车辆」:完整表单可保存,成功后自动选中并关闭选车弹窗
5. 自有 Tab「新增自有车辆」:证书强制与自有管理页一致;成功后回填
6. 「编辑」保存后列表刷新,计划已选车不被覆盖;选车弹窗仍打开
7. 遮罩无 blur;按钮均有图标
- [x] **Step 2: 更新前端图谱**
```bash
./scripts/update-graph.sh frontend
```
或在仓库根目录:`graphify update .`(仅 AST)
- [x] **Step 3: 将设计文档状态改为已实现(实现完成后)**
`docs/superpowers/specs/2026-08-06-pl-plan-vehicle-library-select-design.md` 顶部 `状态:设计中` → `已实现`
---
## Spec 覆盖自检
| Spec 要求 | Task |
|-----------|------|
| 车牌旁按钮「从车辆库选择」 | Task 3 |
| Tab 三方/自有 + 搜索分页 | Task 2 |
| 选择/链接/双击回填 + handleVehicleChange | Task 2 + 3 |
| 新增完整表单(对照管理页) | Task 1 + 2 |
| 编辑 + 刷新列表不自动选中 | Task 1 + 2 |
| 新增成功自动选中关弹窗 | Task 2 |
| 大尺寸 1360 / 列表偏高 | Task 1 + 2 |
| 不改两管理页 | 全任务遵守 |
| 图谱更新 | Task 4 |
@@ -0,0 +1,75 @@
# 合同管理:审批中 / 已审批合同可再编辑并回草稿
## 目标
合同管理支持对「审批中」「已审批(尚未执行)」的合同再次编辑;**编辑保存后**合同变为草稿,可再次提交审批。
采用方案:**编辑保存时回草稿**(不新增独立「退回草稿」动作)。
## 可编辑状态
| 合同状态 `status` | 可否编辑 | 说明 |
|-------------------|----------|------|
| `draft` | 是 | 含审批驳回后回草稿的现有行为 |
| `approving` | 是 | 审批中;保存后作废当前审批流并回草稿 |
| `approved` | 是 | 已审批且尚未开始执行 |
| `executing` / `finished` / `invalid` | 否 | 不在本需求范围;走现有执行 / 变更 / 作废流程 |
## 前端行为
### 打开编辑前确认
对 `approving`、`approved`,在打开编辑弹窗 / 页面之前弹出确认;用户取消则不打开。
| 原状态 | 确认文案 |
|--------|----------|
| `approving` | 编辑保存后将作废当前审批流,合同变回草稿,需重新提交审批 |
| `approved` | 编辑保存后合同将变回草稿,需重新提交审批 |
`draft`(含已驳回)直接打开编辑,无需该确认。
### 入口
以下入口对 `approving`、`approved` 解锁编辑,并走上述确认:
- 合同列表工具栏「编辑」
- 合同详情页 / 查看弹窗「修改」
「提交审批」仍仅 `draft` 可用(保存回草稿后即可再提)。
### 未保存
仅打开编辑、未点保存时,合同状态保持不变。
## 后端行为(更新合同)
放宽更新接口状态校验:允许 `draft` / `approving` / `approved` 更新;其余状态仍拒绝(如「仅草稿 / 审批中 / 已审批(未执行)的合同才能修改」)。
保存成功时统一副作用:
1. `status` 置为 `draft`
2. `approval_status` 清空(`null`)
3. 若原状态为 `approving`:清理待审批记录(复用现有撤回审批的清理逻辑,如 `delete_pending_approvals`)
4. 若原状态为 `approved`:同样清空审批状态;历史审批记录可保留只读展示,不阻塞再次提审
创建与更新字段校验、产品明细等现有业务规则不变;仅放宽状态门禁并增加回草稿副作用。
## 不变范围
- `executing` / `finished` / `invalid` 不可通过本能力直接改合同
- 运输计划等下游仍仅可关联 `approved` / `executing`;合同回草稿后暂时不可新建关联计划,属预期
- 不改审批通过 / 驳回 / 开始执行等既有流转
- 不新增独立「退回草稿」API(本方案在更新保存时完成状态回退)
## 改动落点(实现时)
- `backend/app/crud/crm/contract.py`:`update` 状态门禁;保存后回 `draft` + 清审批状态;审批中清理待审记录
- `web-admin` 合同列表 / 详情 / 查看弹窗 / 表单入口:可编辑状态判断 + 打开编辑前确认文案
## 验收要点
1. 审批中合同:确认 → 编辑 → 保存后为草稿,待审记录已清理,可再提交审批
2. 已审批(未执行)合同:确认 → 编辑 → 保存后为草稿,可再提交审批;保存前下游仍可按原状态关联
3. 执行中 / 已完成 / 已作废:仍不可编辑
4. 草稿 / 已驳回:行为与现网一致,无多余确认
5. 仅打开编辑未保存:状态不变
@@ -1,27 +1,30 @@
# 运输计划「从地址库选择」支持编辑并回填联系方式
**日期:** 2026-08-06
**状态:** 已设计,待实现
**选定方案:** 方案 1 — 选择弹窗内复用新增表单做编辑;选中时按「姓名 电话」回填联系方式;列表去掉坐标列
**状态:** 已实现
**选定方案:** 方案 1 — 选择弹窗内复用新增表单做编辑;选中时按「姓名 电话」回填联系方式;列表去掉坐标列;顶部筛选区改为列表工具栏简易检索
## 1. 目标
1. 在运输计划「从地址库选择」弹窗中,支持编辑已有地址库记录(不离开选择流程)。
2. 选中地址后,若地址库有联系人/联系电话,写入对应「取货联系方式」或「送货联系方式」。
3. 选择弹窗列表不再展示「坐标」列。
4. 去掉顶部 `filter-block` 检索区,改为列表上方工具栏内的简易搜索框;支持按地址名称、详细地址、联系人、电话检索(沿用接口 `q` 参数,后端已覆盖上述字段)。
## 2. 非目标
- 不改地址库管理页(`views/pl/transport-address/index.vue`)结构
- 不抽公共地址表单组件(仍在选择弹窗内复用同一嵌套表单)
- 不改后端 API(沿用现有 `transportAddressApi.update`)
- 不改后端 API(沿用现有 `transportAddressApi.update` / list `q`)
- 编辑成功后不自动选中、不关闭选择弹窗
- 地址库联系人与电话皆空时,不覆盖表单已有联系方式
## 3. 交互
### 3.1 列表
### 3.1 检索与列表
- 移除顶部筛选区块(含「搜索 / 重置」按钮组)。
- 列表工具栏:左侧(或按钮前)放一个 `el-input` 简易检索,placeholder 标明可搜「地址名称、详细地址、联系人或电话」;回车或清空时触发检索;旁侧保留主按钮「新增地址」。
- 移除「坐标」列。
- 操作列增加「编辑」(`Edit` 图标 + 文案),与「选择」并列;操作列宽度适当加大。
- 「选择」、地址名称链接、双击行:行为不变(选中并关闭)。
@@ -43,14 +46,15 @@
- 仅姓名:写入姓名;仅电话:写入电话;皆有:`姓名 电话`;皆空:不修改现有联系方式。
- 取货侧写入 `pickupContact`,送货侧写入 `deliveryContact`。
- 适用页面:运输计划表单弹窗、安排运输弹窗(凡使用 `PlAddressWithLibrary` 处)。
- 组件侧通过可选 `v-model:contact` 回填,便于两侧绑定。
## 4. 改动文件
| 文件 | 变更 |
|------|------|
| `web-admin/src/components/business/pl-transport-address-select-dialog/index.vue` | 去坐标列;操作列编辑;嵌套表单支持新增/编辑 |
| `web-admin/src/components/business/pl-address-with-library/index.vue` | 选中后拼装联系方式并 `emit`(或可选 `v-model:contact`) |
| `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | 取货/送货绑定联系方式回填 |
| `web-admin/src/components/business/pl-transport-address-select-dialog/index.vue` | 去顶部筛选与坐标列;工具栏简易检索;操作列编辑;嵌套表单支持新增/编辑 |
| `web-admin/src/components/business/pl-address-with-library/index.vue` | 选中后拼装联系方式并 `update:contact` |
| `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | 取货/送货绑定 `v-model:contact` |
| `web-admin/src/components/business/pl-plan-arrange-dialog/index.vue` | 同上 |
## 5. 校验与 UI
@@ -62,13 +66,15 @@
## 6. 数据流
```
选择弹窗工具栏 keyword → transportAddressApi.list({ q })
选择弹窗 → 编辑弹窗 → transportAddressApi.update
→ 刷新列表(不 select)
选择 / 新增成功 → emit('select', row)
→ pl-address-with-library:
回填 fullAddress / lng / lat
若有联系信息 → emit('update:contact' | 'select-contact', "姓名 电话")
若有联系信息 → emit('update:contact', "姓名 电话")
→ 父表单写入 pickupContact / deliveryContact
```
@@ -80,3 +86,4 @@
| 地址库联系信息为空 | A:不改动现有联系方式 |
| 编辑交互 | A:编辑保存后刷新列表,不自动选中 |
| 坐标列 | 选择弹窗列表不展示 |
| 检索 UI | 去掉顶部筛选区,改为列表工具栏简易输入框 |
@@ -0,0 +1,103 @@
# 运输计划「从承运商库选择」:选择 + 新增编辑
**日期:** 2026-08-06
**状态:** 已实现
**选定方案:** 方案 1 — 独立选承运商弹窗(对齐「从地址库选择 / 从车辆库选择」)+ 嵌套承运商表单弹窗(对照 `views/pl/carrier` 完整新增编辑)
## 1. 目标
1. 在运输计划新增/编辑弹窗「运输公司」旁增加按钮,打开承运商库选择弹窗。
2. 支持搜索、分页、选择回填计划 `carrierId`。
3. 弹窗内支持 **新增承运商**、**编辑承运商**;表单字段与交互对照承运商管理页(基本信息 + 资质附件)。
4. 新增成功后自动选中并回填;编辑成功仅刷新列表。
## 2. 非目标
- 不改造承运商管理页列表/路由(首期不强制管理页改用新抽组件)
- 不改后端 `carrierApi` 契约
- 不在选承运商弹窗中做批量删除
- 不改安排运输弹窗等其他入口(除非后续单独需求)
## 3. 交互
### 3.1 入口(计划表单)
- 位置:`pl-plan-form-dialog` 运输信息区「运输公司」`el-select` 右侧。
- 布局:与「从车辆库选择 / 从地址库选择」一致(`flex` 行,选择器 `flex:1`,按钮不换行)。
- 按钮文案:**从承运商库选择**(`FolderOpened` + 文案;`btn-ghost`)。
- 点击打开 `PlCarrierLibrarySelectDialog`。
### 3.2 选承运商弹窗
- 标题:从承运商库选择。
- 宽度 **1200px**(或与地址库一致的 1200~1360),`dialog-form-layout`,`align-center`,`append-to-body`,`destroy-on-close`,遮罩无 blur。
- 工具栏:简易搜索(placeholder:搜索承运商名称/联系人/电话,接口 `q`)+ 主按钮「新增承运商」。
- 列表列:序号、承运商名称(主链接选择)、联系人、联系电话、合作状态(Tag)、操作(编辑 / 选择)。
- 选择方式:操作「选择」、名称链接、行双击 → 关闭选弹窗并回填计划。
- 列表 `max-height`:**560**;body `min-height: 560px`。
### 3.3 新增
1. 「新增承运商」打开嵌套 `PlCarrierFormDialog`(无 `carrierId`)。
2. 表单对照 `views/pl/carrier/index.vue`:名称、合作状态、联系人、电话、备注、营业执照 / 道路经营许可证 / 运输协议附件、协议有效期。
3. 确定:`carrierApi.create`;成功后自动 `select` 并关闭选弹窗。
4. 取消:仅关闭表单弹窗。
### 3.4 编辑
1. 「编辑」打开同一表单,加载 `carrierApi.get`。
2. 确定:`carrierApi.update`;成功后刷新列表;选弹窗保持打开,**不**改计划已选承运商。
3. 取消:仅关闭表单弹窗。
### 3.5 回填计划
- 写入 `formData.carrierId`。
- 调用 `ensureSelectedCarrierOption(carrierId)` 保证下拉回显。
- 不强制改车牌/车辆摘要(承运商与车辆独立字段)。
## 4. 组件与改动文件
| 文件 | 变更 |
|------|------|
| `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | 运输公司旁按钮;挂载选弹窗;处理 `select` |
| `web-admin/src/components/business/pl-carrier-library-select-dialog/index.vue` | **新建**:选承运商大弹窗 |
| `web-admin/src/components/business/pl-carrier-form-dialog/index.vue` | **新建**:新增/编辑完整表单 |
实现策略:表单弹窗对照管理页落地;首期管理页可不立刻引用该组件。
## 5. 校验与 UI
- 表单校验与管理页一致(承运商名称必填等)。
- 上传中拦截与管理页相同。
- `dialog-form-layout`,遮罩无 blur,底栏 Close / Check;按钮带图标。
- 合作状态用 `el-tag` 语义色。
## 6. 数据流
```
计划表单 → 「从承运商库选择」→ PlCarrierLibrarySelectDialog
keyword → carrierApi.list({ q, page, pageSize })
选择 / 双击 / 名称链接
→ emit('select', carrier)
→ formData.carrierId = carrier.id
→ ensureSelectedCarrierOption(id)
→ 关闭选弹窗
新增 → PlCarrierFormDialog → carrierApi.create
→ 成功后走「选择」同一路径
编辑 → PlCarrierFormDialog → carrierApi.update
→ 刷新列表(不 select、不关选弹窗)
```
## 7. 决策记录
| 项 | 结论 |
|----|------|
| 弹窗用途 | A:选承运商为主,兼新增/编辑 |
| 表单范围 | A:完整对照承运商管理页 |
| 按钮文案 | 从承运商库选择 |
| 新增成功 | 自动选中并关闭选弹窗 |
| 编辑成功 | 仅刷新列表 |
| 管理页 | 首期不强制抽离改造 |
@@ -0,0 +1,40 @@
# 危化品证上传:仅展示危化计划 +「未上传证书」Tab
## 背景
【危化品证上传】页当前复用运输计划列表,未按危化属性过滤,大量无需上传证书的计划干扰操作。
## 需求
1. 本页所有单据类型 Tab(全部 / 入库 / 出库 / 直销)仅展示至少含一件易制毒或易制爆产品的运输计划。
2. 在「直销单」后增加 Tab「未上传证书」:含危化产品,且至少一件产品缺少必需证书附件。
3. 「未上传证书」判定只看附件是否齐全,与「证书齐备」标记无关。
## 证书缺口规则(与现网一致)
- 易制毒:须有购买证、运输证(`purchaseCertUrl` / `transportCertUrl`)
- 易制爆:须有合法证明(`legalUseCertUrl`)
- 危化标识以 CRM 产品主数据为准(`is_toxic` / `is_explosive`),不信任计划产品快照
## 方案
后端 `GET /transport/plan/` 增加可选查询参数:
| 参数 | 含义 |
|------|------|
| `hasHazardProduct=1` | 计划存在危化产品(CRM) |
| `certUploadStatus=incomplete` | 存在危化产品且至少一张必需证书 URL 为空 |
本页默认始终传 `hasHazardProduct=1`;切到「未上传证书」时额外传 `certUploadStatus=incomplete`,并清空 `planType`。
运输计划、归档等其它页面不传上述参数,行为不变。
## 前端
- `PlPlanTypeTabs` 支持可选 `tabs` 覆盖,本页传入含「未上传证书」的 Tab 列表。
- Tab 值 `cert_incomplete` 与单据类型互斥。
## 非目标
- 不改证书齐备业务语义
- 不改菜单角标算法(若后续要对齐可另开需求)
@@ -0,0 +1,41 @@
# 运输计划取货/送货联系方式
## Goal
在运输计划取货信息、送货信息中各增加一个普通文本「联系方式」字段,并调整表单两行布局;查看、安排车辆、出入库、公开 H5 同步展示。
## Decision
方案 A:仅在 `pl_plan` 落库两字段;出入库/公开页经现有 `merge_plan_logistics_into_order` / 公开接口从计划带出。
## Data
| DB | API | 类型 | 说明 |
|----|-----|------|------|
| `pickup_contact` | `pickupContact` | varchar(200) NULL | 取货联系方式,非必填 |
| `delivery_contact` | `deliveryContact` | varchar(200) NULL | 送货联系方式,非必填 |
无手机号格式校验。
## UI Layout
取货 / 送货顺序(`form-grid-2`):
1. 地址(整行)
2. 时间 | 联系方式
3. 注意事项(整行)
查看态与公开页顺序一致。时间标签为「取货时间 / 送货时间」。
## Surfaces
1. 计划新增/编辑弹窗
2. 计划查看
3. 安排车辆弹窗(及通知文案若含注意事项则一并带出联系方式)
4. 出入库列表/查看(计划合并字段)
5. 公开运输 / 出入库 / 站点 H5
## Out of scope
- 出入库表独立落库联系方式
- 复用合同 `contactName` / `contactPhone`
@@ -0,0 +1,111 @@
# 运输计划「从车辆库选择」:三方 / 自有 Tab + 新增编辑
**日期:** 2026-08-06
**状态:** 已实现
**选定方案:** 方案 1 — 独立选车弹窗(对齐「从地址库选择」)+ 嵌套车辆表单弹窗(对照三方车辆 / 山峡自有车辆管理页已有新增编辑弹窗)
## 1. 目标
1. 在运输计划新增/编辑弹窗「车牌号」旁增加按钮,打开车辆库选择弹窗。
2. 选择弹窗分两个 Tab:**三方车辆**、**自有车辆**;支持搜索、分页、选择回填。
3. 各 Tab 内支持 **新增车辆**、**编辑车辆**;表单字段与交互对照现有管理页弹窗(基本信息 + 关联人员 + 车辆证书)。
4. 选车弹窗与表单弹窗均采用较大宽高,便于操作。
## 2. 非目标
- 不改造三方车辆、山峡自有车辆管理页本身的列表/路由逻辑(首期不强制两管理页改用新抽组件)
- 不改后端车辆 API 契约(沿用 `vehicleApi.list/get/create/update`)
- 不在选车弹窗中做粘贴识别、批量删除
- 不改安排运输弹窗等其他入口(除非后续单独需求)
## 3. 交互
### 3.1 入口(计划表单)
- 位置:`pl-plan-form-dialog` 运输信息区「车牌号」`el-select` 右侧。
- 布局:与 `PlAddressWithLibrary` 一致(`flex` 行,选择器 `flex:1`,按钮不换行)。
- 按钮文案:**从车辆库选择**(`FolderOpened` 图标 + 文案;`btn-ghost` 中性描边,非主色实心)。
- 点击打开 `PlVehicleLibrarySelectDialog`。
### 3.2 选车弹窗
- 标题:从车辆库选择。
- 宽度 **1360px**,`dialog-form-layout`,`align-center`,`append-to-body`,`destroy-on-close`,遮罩无 blur。
- 顶栏 `el-tabs`:`三方车辆`(`carrier`)/ `自有车辆`(`self`);切换 Tab 重置页码并重新拉列表。
- 工具栏:简易搜索(placeholder 标明可搜车牌号,接口 `q`)+ 主按钮「新增车辆」。
- 列表列:序号、车牌号(主链接选择)、挂车号、车辆状态(Tag)、载重(吨)、承运商(仅三方 Tab 显示)、关联人员摘要、操作。
- 操作列:**编辑**、**选择**(与地址库一致);`fixed="right"`,`min-width` 足够容纳两按钮。
- 选择方式:操作「选择」、车牌链接、行双击 → 关闭选车弹窗并回填计划。
- 列表 `max-height`:**560**;body 区域偏高,避免一屏过矮。
### 3.3 新增
1. 点击「新增车辆」打开嵌套 `PlVehicleFormDialog`,`vehicleType` 锁定为当前 Tab。
2. 表单对照:
- 三方:`views/pl/vehicle/index.vue` 新增弹窗(含关联人员、证书;证书校验策略与三方页一致)。
- 自有:`views/pl/self-vehicle/index.vue` 新增弹窗(含 `enforce-self-vehicle-required` 等自有规则)。
3. 确定:校验通过后 `vehicleApi.create`;成功提示「新增成功」,关闭表单弹窗,对返回车辆执行与「选择」相同逻辑(回填并关闭选车弹窗)。
4. 取消:仅关闭表单弹窗。
### 3.4 编辑
1. 点击「编辑」打开同一表单组件;标题:三方为「编辑车辆」,自有为「编辑自有车辆」。
2. 加载 `vehicleApi.get` 回填基本信息、人员、证书。
3. 确定:`vehicleApi.update`;成功提示「编辑成功」,关闭表单弹窗,**刷新当前 Tab 列表**;选车弹窗保持打开,**不**自动改计划已选车辆。
4. 取消:仅关闭表单弹窗。
### 3.5 回填计划
- 写入 `formData.vehicleId`。
- 调用现有 `handleVehicleChange(vehicleId)`:刷新 `PlVehicleSummaryStrip`、回填承运商、默认司机等,行为与下拉选车一致。
- 若下拉选项尚未包含新车,需将车辆加入 `vehicleList`(或触发一次 `searchVehicles`)以保证回显。
## 4. 组件与改动文件
| 文件 | 变更 |
|------|------|
| `web-admin/src/components/business/pl-plan-form-dialog/index.vue` | 车牌旁按钮;挂载选车弹窗;处理 `select` |
| `web-admin/src/components/business/pl-vehicle-library-select-dialog/index.vue` | **新建**:选车大弹窗(Tab + 列表 + 搜索 + 挂载表单弹窗) |
| `web-admin/src/components/business/pl-vehicle-form-dialog/index.vue` | **新建**:新增/编辑车辆完整表单;`vehicleType` prop 区分三方/自有 |
实现策略:表单弹窗**对照**两管理页已有弹窗模式落地(字段、人员子表、证书组件、校验);首期管理页可不立刻改为引用该组件,避免扩大范围。
## 5. 校验与 UI
- 表单校验、人员归属兼容、证书必填规则:分别与对应管理页一致。
- 选车/表单弹窗:`dialog-form-layout`,遮罩 `backdrop-filter: none`,底栏 Close / Check。
- 所有带文案按钮带前置 `el-icon`。
- 状态列用 `el-tag` 语义色(与车辆列表一致)。
- 列表主标识(车牌)用 `el-link type="primary"` 触发选择。
## 6. 数据流
```
计划表单 → 「从车辆库选择」→ PlVehicleLibrarySelectDialog
Tab(carrier|self) + keyword → vehicleApi.list({ vehicleType, q, page, pageSize })
选择 / 双击 / 车牌链接
→ emit('select', vehicle)
→ 计划 formData.vehicleId = vehicle.id
→ handleVehicleChange(vehicleId)
→ 关闭选车弹窗
新增 → PlVehicleFormDialog(vehicleType) → vehicleApi.create
→ 成功后走「选择」同一路径
编辑 → PlVehicleFormDialog → vehicleApi.update
→ 刷新当前 Tab 列表(不 select、不关选车弹窗)
```
## 7. 决策记录
| 项 | 结论 |
|----|------|
| 弹窗用途 | A:选车为主,兼新增/编辑(对齐地址库) |
| 表单范围 | A:完整表单(基本信息 + 人员 + 证书),对照管理页 |
| 操作列 | A:选择 + 编辑 |
| 实现方案 | 方案 1:独立选车弹窗 + 新建表单组件;管理页首期不强制抽离改造 |
| 按钮文案 | 从车辆库选择 |
| 弹窗尺寸 | 选车与表单均约 1360px 宽,列表区偏高 |
| 新增成功 | 自动选中并关闭选车弹窗 |
| 编辑成功 | 仅刷新列表,不自动选中 |