Files
shanxia2.0/docs/superpowers/plans/2026-08-06-pl-plan-vehicle-library-select.md
T
2026-08-06 09:58:20 +08:00

368 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运输计划「从车辆库选择」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 |