368 lines
12 KiB
Markdown
368 lines
12 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:** 在运输计划新增/编辑弹窗车牌号旁提供「从车辆库选择」,弹窗分三方/自有 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 |
|