提交
This commit is contained in:
1 parent
67d37a1f0e
commit
54a1ba0c5c
21 files changed
+8646
-7577
No files matched your search
@@ -1 +0,0 @@
|
||||
22475
|
||||
@@ -0,0 +1,47 @@
|
||||
# 地磅可选车辆(皖H21522)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:** 车牌为皖H21522 时,内部客户段地磅默认不勾且可手动勾选;换车时按规则重算各段。
|
||||
|
||||
**Architecture:** 车牌例外判定集中在工具函数;表单弹窗传入当前车牌并在换车时重算;装货/送货段按「可否手动」控制 checkbox。
|
||||
|
||||
**Tech Stack:** Vue 3 + TypeScript(web-admin)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 工具函数与默认值
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/utils/pl-internal-customer.ts`
|
||||
- Modify: `web-admin/src/utils/pl-plan-stop-form.ts`
|
||||
|
||||
- [x] 增加 `PL_DIBANG_OPTIONAL_VEHICLE_PLATES` / `isPlDibangOptionalVehiclePlate`
|
||||
- [x] `resolveStopDibangDefaults` 增加可选 `vehiclePlate`:可选车时内部客户也返回未勾选
|
||||
- [x] 新增 `canManualToggleStopDibang(customerName, vehiclePlate?)`
|
||||
- [x] `normalizeStopDibangFieldsFromApi`:可选车时不强制 gen=1,保留 API 值;仍禁止外部客户 gen=1
|
||||
|
||||
### Task 2: 装货/送货段 UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/components/business/pl-plan-load-section/index.vue`
|
||||
- Modify: `web-admin/src/components/business/pl-plan-delivery-section/index.vue`
|
||||
|
||||
- [x] 新增 prop `vehiclePlate`
|
||||
- [x] checkbox `disabled` 绑定 `!canManualToggle`
|
||||
- [x] `handleCustomerChange` / `handleDibangToggle` 使用车牌感知的 defaults / canManual
|
||||
- [x] 更新 hint 文案
|
||||
|
||||
### Task 3: 表单弹窗换车联动
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/components/business/pl-plan-form-dialog/index.vue`
|
||||
|
||||
- [x] computed 当前车牌传给两段组件
|
||||
- [x] `handleVehicleChange`(及清空车辆)后调用 `reapplyStopDibangByVehiclePlate`
|
||||
- [x] 编辑打开时 `syncStopDibang: false`,不强制覆盖已存值
|
||||
|
||||
### Task 4: 验证
|
||||
|
||||
- [x] 单元测试:`pl-dibang-optional-vehicle.test.ts`(5 passed)
|
||||
- [x] `graphify update`(frontend)
|
||||
@@ -0,0 +1,44 @@
|
||||
# 运输计划「地磅可选车辆」设计
|
||||
|
||||
日期:2026-09-02
|
||||
范围:`web-admin` 运输计划新增/编辑弹窗(装货/送货段地磅勾选)
|
||||
状态:已实现
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
默认规则:装货/送货段客户为内部客户(安庆市山峡化工有限公司 / 安徽云栋化工有限公司)时,「生成地磅单」默认勾选且不可改;其他客户不勾且不可勾。
|
||||
|
||||
业务例外:当计划车牌为 **皖H21522** 时,即使段次客户为内部客户,地磅单也默认不勾选,但勾选框放开,由用户自行决定。
|
||||
|
||||
## 2. 已确认决策
|
||||
|
||||
| 项 | 决策 |
|
||||
|---|---|
|
||||
| 匹配方式 | 主牌精确匹配 `皖H21522`(不含挂车号拼接) |
|
||||
| 换到该车 | 立刻取消所有装货/送货段地磅勾选,并放开勾选框(内部客户可再勾) |
|
||||
| 换离该车 | 立刻恢复原规则:内部客户自动勾选并锁定;外部客户不勾且锁定 |
|
||||
| 编辑回填 | 保留接口已存勾选值;若当前为该车 + 内部客户,仅放开勾选框 |
|
||||
| 外部客户 | 行为不变:始终不勾、不可勾(与是否该车无关) |
|
||||
|
||||
## 3. 行为矩阵
|
||||
|
||||
| 车牌 | 段客户 | 默认勾选 | 可手动改 |
|
||||
|---|---|---|---|
|
||||
| 非 皖H21522 | 内部 | 是(装货出库 / 送货入库) | 否 |
|
||||
| 非 皖H21522 | 外部/空 | 否 | 否 |
|
||||
| 皖H21522 | 内部 | 否 | 是 |
|
||||
| 皖H21522 | 外部/空 | 否 | 否 |
|
||||
|
||||
## 4. 实现要点
|
||||
|
||||
- 常量与判定:`isPlDibangOptionalVehiclePlate(plate)`(与 `pl-internal-customer` 同层或同文件旁)
|
||||
- `resolveStopDibangDefaults(customerName, stopType, vehiclePlate?)`:可选车牌参与默认值
|
||||
- `canManualToggleStopDibang(customerName, vehiclePlate?)`:仅「内部 + 可选车」为 true
|
||||
- 表单弹窗:根据当前选中车辆 `licensePlate` 传给装货/送货段;`handleVehicleChange` 后对所有段重算地磅字段
|
||||
- 装货/送货段:`disabled` 改为 `!canManualToggle`;换客户时传入当前车牌计算默认值;提示文案区分「默认勾选 / 可选 / 仅内部可生成」
|
||||
|
||||
## 5. 非目标
|
||||
|
||||
- 不改后端地磅生成规则(仍以段上 `generateDibang` 为准)
|
||||
- 不扩展其他车牌(后续可加常量列表)
|
||||
- 不改列表/查看态展示逻辑(只读展示已存字段)
|
||||
Binary file not shown.
@@ -1405,8 +1405,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-plan-archive-dialog/index.vue": {
|
||||
"mtime": 1787980948.1158607,
|
||||
"ast_hash": "040d5dd36f9b7df2cc4ac2f590723b9a",
|
||||
"mtime": 1788322435.9910197,
|
||||
"ast_hash": "4374595c9fb44d3a8858b7950fe2e0f7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-plan-archive-segment-block/index.vue": {
|
||||
@@ -1455,8 +1455,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-plan-form-dialog/index.vue": {
|
||||
"mtime": 1788320533.5558727,
|
||||
"ast_hash": "834077c92f875e01374020756489c3d4",
|
||||
"mtime": 1788322417.4664786,
|
||||
"ast_hash": "e0a061a85811f3260f409ba7d148c804",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-plan-hazard-cert-view-table/index.vue": {
|
||||
@@ -1535,8 +1535,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-transport-address-select-dialog/index.vue": {
|
||||
"mtime": 1788321576.6438992,
|
||||
"ast_hash": "b7cb44dc6cebdbb6df609195ef57055a",
|
||||
"mtime": 1788321898.0268815,
|
||||
"ast_hash": "58ccb0dd6d31c3e561fc3e7d7fd97aeb",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/components/business/pl-vehicle-cert-badges/index.vue": {
|
||||
|
||||
@@ -286,9 +286,10 @@
|
||||
"284": "Community 284",
|
||||
"285": "Community 285",
|
||||
"286": "Community 286",
|
||||
"287": "index.vue",
|
||||
"288": "index.vue",
|
||||
"289": "handleEdit",
|
||||
"290": "index.js",
|
||||
"291": "form.vue",
|
||||
"292": "Community 292",
|
||||
"293": "index.vue",
|
||||
"294": "menuFavorite.ts",
|
||||
@@ -296,6 +297,7 @@
|
||||
"296": "Community 296",
|
||||
"298": "error-handle.ts",
|
||||
"299": "zh_CN.js",
|
||||
"300": "emit",
|
||||
"301": "ProcessStatusTag.vue",
|
||||
"302": "Community 302",
|
||||
"303": "Community 303",
|
||||
@@ -306,7 +308,6 @@
|
||||
"308": "Community 308",
|
||||
"309": "Community 309",
|
||||
"310": "Community 310",
|
||||
"312": "loadUserOptions",
|
||||
"314": "assignmentMatchesFilter",
|
||||
"315": "Community 315",
|
||||
"316": "Community 316",
|
||||
@@ -336,7 +337,6 @@
|
||||
"340": "Community 340",
|
||||
"341": "Community 341",
|
||||
"342": "error-handle.ts",
|
||||
"343": "v2.4.2.1 - 2025-06-16",
|
||||
"344": "ArtException.vue",
|
||||
"346": "Community 346",
|
||||
"347": "Community 347",
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/wuzilong/work/00code/shanxia2.0/web-admin
|
||||
.
|
||||
@@ -286,8 +286,8 @@
|
||||
"284": "Community 284",
|
||||
"285": "Community 285",
|
||||
"286": "Community 286",
|
||||
"287": "pl-batch-print.ts",
|
||||
"288": "syncStatsColumnWidths",
|
||||
"287": "index.vue",
|
||||
"288": "index.vue",
|
||||
"289": "handleEdit",
|
||||
"290": "index.js",
|
||||
"291": "form.vue",
|
||||
@@ -296,10 +296,10 @@
|
||||
"294": "menuFavorite.ts",
|
||||
"295": "Community 295",
|
||||
"296": "Community 296",
|
||||
"297": "zh_CN.js",
|
||||
"297": "pool.ts",
|
||||
"298": "error-handle.ts",
|
||||
"299": "zh_CN.js",
|
||||
"300": "index.vue",
|
||||
"300": "emit",
|
||||
"301": "ProcessStatusTag.vue",
|
||||
"302": "Community 302",
|
||||
"303": "Community 303",
|
||||
@@ -310,9 +310,8 @@
|
||||
"308": "Community 308",
|
||||
"309": "Community 309",
|
||||
"310": "Community 310",
|
||||
"311": "index.vue",
|
||||
"311": "pad",
|
||||
"312": "loadUserOptions",
|
||||
"313": "formatKgToTon",
|
||||
"314": "assignmentMatchesFilter",
|
||||
"315": "Community 315",
|
||||
"316": "Community 316",
|
||||
@@ -342,12 +341,9 @@
|
||||
"340": "Community 340",
|
||||
"341": "Community 341",
|
||||
"342": "error-handle.ts",
|
||||
"343": "v2.4.2.1 - 2025-06-16",
|
||||
"344": "ArtException.vue",
|
||||
"345": "loadUserOptions",
|
||||
"346": "Community 346",
|
||||
"347": "Community 347",
|
||||
"348": "pad",
|
||||
"349": "Community 349",
|
||||
"350": "Community 350",
|
||||
"355": "Community 355",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+3582
-3121
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -205,11 +205,25 @@
|
||||
:bundle="vehicleBundle"
|
||||
/>
|
||||
|
||||
<PlPlanFormDialog
|
||||
v-model:visible="planEditFormVisible"
|
||||
:plan-id="planId"
|
||||
plan-edit-only
|
||||
@success="handlePlanEditSuccess"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer pl-arrange-footer">
|
||||
<el-button @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:disabled="loading || !planId || plan.followStatus === 'cancelled'"
|
||||
@click="handleOpenPlanEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑运输计划
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!isAdjustMode"
|
||||
:loading="draftLoading"
|
||||
@@ -226,7 +240,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Document } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Document, Edit } from '@element-plus/icons-vue'
|
||||
import { fetchProductDetail, type ProductDetail } from '@/api/crm/product'
|
||||
import {
|
||||
inboundOrderApi,
|
||||
@@ -244,6 +258,7 @@
|
||||
import PlPlanArchiveSegmentBlock, {
|
||||
type ArchiveSegmentMeta
|
||||
} from '@/components/business/pl-plan-archive-segment-block/index.vue'
|
||||
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
|
||||
import PlPlanStopsSplitShell from '@/components/business/pl-plan-stops-split-shell/index.vue'
|
||||
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
|
||||
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
|
||||
@@ -292,6 +307,7 @@
|
||||
const activeTab = ref<'archive' | 'overview'>('archive')
|
||||
const tabsRef = ref<InstanceType<typeof PlPlanViewTabs> | null>(null)
|
||||
const vehicleViewVisible = ref(false)
|
||||
const planEditFormVisible = ref(false)
|
||||
const plan = ref<Partial<Plan>>({})
|
||||
|
||||
const {
|
||||
@@ -1193,7 +1209,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpened() {
|
||||
async function reloadArchiveContent() {
|
||||
if (!props.planId) return
|
||||
loading.value = true
|
||||
resetState()
|
||||
@@ -1239,14 +1255,44 @@
|
||||
await applyFreightPrefill(data)
|
||||
initFeedbackAttachmentEdits()
|
||||
} catch (error) {
|
||||
innerVisible.value = false
|
||||
const message = error instanceof Error ? error.message : '加载运输计划失败'
|
||||
ElMessage.error(message.includes('加载') ? message : `加载运输计划失败:${message}`)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenPlanEdit() {
|
||||
if (!props.planId || plan.value.followStatus === 'cancelled') return
|
||||
ElMessage.info({
|
||||
message: '修改计划信息后,归档填写区将自动刷新,请核对装卸货数量与品牌归档。',
|
||||
duration: 4000
|
||||
})
|
||||
planEditFormVisible.value = true
|
||||
}
|
||||
|
||||
async function handlePlanEditSuccess() {
|
||||
planEditFormVisible.value = false
|
||||
activeTab.value = 'archive'
|
||||
try {
|
||||
await reloadArchiveContent()
|
||||
tabsRef.value?.reload()
|
||||
emit('success')
|
||||
} catch {
|
||||
// reloadArchiveContent 已提示错误
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpened() {
|
||||
if (!props.planId) return
|
||||
try {
|
||||
await reloadArchiveContent()
|
||||
} catch {
|
||||
innerVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
// destroy-on-close 由 el-dialog 处理
|
||||
}
|
||||
@@ -1450,7 +1496,7 @@
|
||||
|
||||
.pl-archive-tabs {
|
||||
:deep(> .el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 仅用顶部导航,内容区由下方面板接管 */
|
||||
@@ -1461,6 +1507,7 @@
|
||||
|
||||
.pl-archive-tab-panel {
|
||||
min-height: 360px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pl-arrange-form-scroll {
|
||||
|
||||
@@ -160,13 +160,19 @@
|
||||
<div class="pl-plan-dibang-row">
|
||||
<el-checkbox
|
||||
:model-value="delivery.generateDibang === 1"
|
||||
disabled
|
||||
:disabled="readonly || !canManualDibang(delivery.customerName)"
|
||||
@update:model-value="(checked) => handleDibangToggle(index, checked)"
|
||||
>
|
||||
生成地磅单({{ directionLabel(delivery.dibangDirection) }})
|
||||
</el-checkbox>
|
||||
<span
|
||||
v-if="isPlInternalCustomerName(delivery.customerName)"
|
||||
v-if="canManualDibang(delivery.customerName)"
|
||||
class="pl-plan-dibang-row__hint"
|
||||
>
|
||||
该车辆地磅单可选,请自行勾选
|
||||
</span>
|
||||
<span
|
||||
v-else-if="isPlInternalCustomerName(delivery.customerName)"
|
||||
class="pl-plan-dibang-row__hint"
|
||||
>
|
||||
本段客户为内部客户时默认勾选
|
||||
@@ -232,6 +238,7 @@
|
||||
import PlPlanStopProductTable from '@/components/business/pl-plan-stop-product-table/index.vue'
|
||||
import { resolveAddressNotesFill } from '@/utils/pl-address-notes-fill'
|
||||
import {
|
||||
canManualToggleStopDibang,
|
||||
createEmptyDeliveryForm,
|
||||
resolveStopDibangDefaults,
|
||||
sumStopProductsPlanQty,
|
||||
@@ -254,6 +261,8 @@
|
||||
excludePlanId?: string
|
||||
/** 装货段产品,用于「送货产品不在装货列表」软提醒 */
|
||||
loadProducts?: PlanStopProductFormRow[]
|
||||
/** 当前计划车牌(主牌),用于地磅可选车例外 */
|
||||
vehiclePlate?: string
|
||||
}>(),
|
||||
{
|
||||
tabMode: true,
|
||||
@@ -262,7 +271,8 @@
|
||||
qtyUnitLabel: '',
|
||||
showSplitHint: false,
|
||||
excludePlanId: '',
|
||||
loadProducts: () => []
|
||||
loadProducts: () => [],
|
||||
vehiclePlate: ''
|
||||
}
|
||||
)
|
||||
|
||||
@@ -535,7 +545,7 @@
|
||||
(item) => String(item.id) === String(customerId)
|
||||
)
|
||||
const customerName = customer?.customerName || ''
|
||||
const defaults = resolveStopDibangDefaults(customerName, 'delivery')
|
||||
const defaults = resolveStopDibangDefaults(customerName, 'delivery', props.vehiclePlate)
|
||||
patchDelivery(index, {
|
||||
customerId: String(customerId),
|
||||
customerName,
|
||||
@@ -573,11 +583,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function canManualDibang(customerName?: string | null) {
|
||||
return canManualToggleStopDibang(customerName, props.vehiclePlate)
|
||||
}
|
||||
|
||||
function handleDibangToggle(index: number, checked: boolean) {
|
||||
const delivery = props.modelValue[index]
|
||||
// 仅山峡/云栋可生成地磅单,且由客户类型自动决定,不允许手动勾选
|
||||
if (!isPlInternalCustomerName(delivery?.customerName)) {
|
||||
patchDelivery(index, { generateDibang: 0, dibangDirection: 'none' })
|
||||
if (!canManualDibang(delivery?.customerName)) {
|
||||
if (!isPlInternalCustomerName(delivery?.customerName)) {
|
||||
patchDelivery(index, { generateDibang: 0, dibangDirection: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
patchDelivery(index, {
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
v-model="loadForm"
|
||||
compact-products
|
||||
:exclude-plan-id="planId || ''"
|
||||
:vehicle-plate="currentVehiclePlate"
|
||||
@open-customer-picker="openCustomerSelector('load')"
|
||||
@add-products="openProductSelector('load')"
|
||||
/>
|
||||
@@ -233,6 +234,7 @@
|
||||
:show-split-hint="showDeliverySplitHint"
|
||||
:exclude-plan-id="planId || ''"
|
||||
:load-products="loadForm.products"
|
||||
:vehicle-plate="currentVehiclePlate"
|
||||
@active-index-change="deliveryActiveIndex = $event"
|
||||
@open-customer-picker="openCustomerSelector('delivery', $event)"
|
||||
@sync-products="syncDeliveryProductsFromLoadManual"
|
||||
@@ -377,6 +379,7 @@
|
||||
enrichStopProductsDisplayFromPlan,
|
||||
mapLoadProductsToPlanFormRows,
|
||||
mergePlanProductsFromLoadMirror,
|
||||
resolveStopDibangDefaults,
|
||||
sumStopProductsPlanQty,
|
||||
syncDeliveriesProductsFromLoad,
|
||||
type PlanDeliveryForm,
|
||||
@@ -426,6 +429,8 @@
|
||||
copyFromPlanId?: string
|
||||
/** 从列表/详情「安排运输」「调整运输」进入时展开安排区块 */
|
||||
openForArrange?: boolean
|
||||
/** 归档等场景:仅保存计划,不展示暂存/安排运输(已安排计划也显示「保存」) */
|
||||
planEditOnly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -497,14 +502,19 @@
|
||||
const selectedDriverId = ref('')
|
||||
|
||||
/** 已安排:禁止仅「保存」,须走保存并安排/调整运输,避免计划与地磅等联动字段脱节 */
|
||||
const showSaveButton = computed(() => loadedTransportStatus.value !== 'arranged')
|
||||
const showSaveButton = computed(() => {
|
||||
if (props.planEditOnly && isEdit.value) return true
|
||||
return loadedTransportStatus.value !== 'arranged'
|
||||
})
|
||||
const showDraftButton = computed(() => {
|
||||
if (props.planEditOnly) return false
|
||||
if (!isEdit.value) return true
|
||||
return (
|
||||
loadedFollowStatus.value === 'drafting' || loadedFollowStatus.value === 'pending_arrange'
|
||||
)
|
||||
})
|
||||
const showSaveAndArrangeButton = computed(() => {
|
||||
if (props.planEditOnly) return false
|
||||
if (!isEdit.value) return true
|
||||
if (props.openForArrange && loadedTransportStatus.value === 'arranged') return false
|
||||
return loadedTransportStatus.value === 'pending' || loadedTransportStatus.value === 'arranged'
|
||||
@@ -512,7 +522,7 @@
|
||||
const isRearrangeMode = computed(
|
||||
() => loadedTransportStatus.value === 'arranged' && Boolean(props.openForArrange)
|
||||
)
|
||||
const showRearrangeButton = computed(() => isRearrangeMode.value)
|
||||
const showRearrangeButton = computed(() => !props.planEditOnly && isRearrangeMode.value)
|
||||
const saveButtonLabel = computed(() => (isEdit.value ? '保存' : '创建'))
|
||||
const saveAndArrangeButtonLabel = computed(() => {
|
||||
if (!isEdit.value) return '创建并安排运输'
|
||||
@@ -766,6 +776,35 @@
|
||||
vehicleList.value.find((item) => item.id === formData.vehicleId)
|
||||
)
|
||||
|
||||
const currentVehiclePlate = computed(() => {
|
||||
const fromList = (selectedVehicle.value?.licensePlate || '').trim()
|
||||
if (fromList) return fromList
|
||||
return (vehicleBundle.value?.detail?.licensePlate || '').trim()
|
||||
})
|
||||
|
||||
/** 按当前车牌重算各段地磅默认勾选(换车时调用;编辑回填勿调用以免覆盖已存值) */
|
||||
function reapplyStopDibangByVehiclePlate(plate?: string | null) {
|
||||
const vehiclePlate = (plate ?? currentVehiclePlate.value) || ''
|
||||
const loadDefaults = resolveStopDibangDefaults(
|
||||
loadForm.value.customerName,
|
||||
'load',
|
||||
vehiclePlate
|
||||
)
|
||||
loadForm.value = {
|
||||
...loadForm.value,
|
||||
generateDibang: loadDefaults.generateDibang,
|
||||
dibangDirection: loadDefaults.dibangDirection
|
||||
}
|
||||
deliveryForms.value = deliveryForms.value.map((row) => {
|
||||
const defaults = resolveStopDibangDefaults(row.customerName, 'delivery', vehiclePlate)
|
||||
return {
|
||||
...row,
|
||||
generateDibang: defaults.generateDibang,
|
||||
dibangDirection: defaults.dibangDirection
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const summaryCarrierName = computed(() => {
|
||||
const carrier = carrierList.value.find((item) => item.id === formData.carrierId)
|
||||
if (carrier?.carrierName) return carrier.carrierName
|
||||
@@ -1050,9 +1089,10 @@
|
||||
|
||||
function applyStopsFromPlan(data: Plan) {
|
||||
stopFormsReady.value = false
|
||||
const plate = data.licensePlate || ''
|
||||
if (data.load || data.deliveries?.length) {
|
||||
loadForm.value = mapLoadFromApi(data.load)
|
||||
deliveryForms.value = mapDeliveriesFromApi(data.deliveries)
|
||||
loadForm.value = mapLoadFromApi(data.load, plate)
|
||||
deliveryForms.value = mapDeliveriesFromApi(data.deliveries, plate)
|
||||
} else {
|
||||
const legacy = mapLegacyPlanToStops(data)
|
||||
loadForm.value = legacy.load
|
||||
@@ -1696,7 +1736,7 @@
|
||||
|
||||
if (data.vehicleId) {
|
||||
await ensureSelectedVehicleOption(data.vehicleId)
|
||||
await handleVehicleChange(data.vehicleId)
|
||||
await handleVehicleChange(data.vehicleId, { syncStopDibang: false })
|
||||
}
|
||||
if (data.driverId) {
|
||||
selectedDriverId.value = data.driverId
|
||||
@@ -1762,6 +1802,7 @@
|
||||
formData.carrierId = ''
|
||||
selectedDriverId.value = ''
|
||||
vehicleBundle.value = null
|
||||
reapplyStopDibangByVehiclePlate('')
|
||||
freightCalc.resetFreightCalc()
|
||||
syncSelectedCustomerOptions()
|
||||
} catch (e) {
|
||||
@@ -1790,11 +1831,16 @@
|
||||
selectedDriverId.value = defaultDriver?.personnelId || ''
|
||||
}
|
||||
|
||||
async function handleVehicleChange(vehicleId: string) {
|
||||
async function handleVehicleChange(
|
||||
vehicleId: string,
|
||||
opts?: { syncStopDibang?: boolean }
|
||||
) {
|
||||
const syncStopDibang = opts?.syncStopDibang !== false
|
||||
vehicleBundle.value = null
|
||||
selectedDriverId.value = ''
|
||||
if (!vehicleId) {
|
||||
formData.carrierId = ''
|
||||
if (syncStopDibang) reapplyStopDibangByVehiclePlate('')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1818,6 +1864,12 @@
|
||||
} finally {
|
||||
vehicleLoading.value = false
|
||||
}
|
||||
if (syncStopDibang) {
|
||||
const plate =
|
||||
(vehicle?.licensePlate || '').trim() ||
|
||||
(vehicleBundle.value?.detail?.licensePlate || '').trim()
|
||||
reapplyStopDibangByVehiclePlate(plate)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVehicleLibrarySelect(vehicle: Vehicle) {
|
||||
|
||||
@@ -108,12 +108,15 @@
|
||||
<div class="pl-plan-dibang-row">
|
||||
<el-checkbox
|
||||
:model-value="modelValue.generateDibang === 1"
|
||||
disabled
|
||||
:disabled="readonly || !canManualDibang"
|
||||
@update:model-value="handleDibangToggle"
|
||||
>
|
||||
生成地磅单({{ dibangDirectionLabel }})
|
||||
</el-checkbox>
|
||||
<span v-if="isInternal" class="pl-plan-dibang-row__hint">本段客户为内部客户时默认勾选</span>
|
||||
<span v-if="canManualDibang" class="pl-plan-dibang-row__hint">
|
||||
该车辆地磅单可选,请自行勾选
|
||||
</span>
|
||||
<span v-else-if="isInternal" class="pl-plan-dibang-row__hint">本段客户为内部客户时默认勾选</span>
|
||||
<span v-else-if="modelValue.customerId" class="pl-plan-dibang-row__hint">
|
||||
仅安庆山峡/安徽云栋可生成地磅单
|
||||
</span>
|
||||
@@ -162,7 +165,11 @@
|
||||
import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/index.vue'
|
||||
import PlPlanStopProductTable from '@/components/business/pl-plan-stop-product-table/index.vue'
|
||||
import { resolveAddressNotesFill } from '@/utils/pl-address-notes-fill'
|
||||
import { resolveStopDibangDefaults, type PlanLoadForm } from '@/utils/pl-plan-stop-form'
|
||||
import {
|
||||
canManualToggleStopDibang,
|
||||
resolveStopDibangDefaults,
|
||||
type PlanLoadForm
|
||||
} from '@/utils/pl-plan-stop-form'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
|
||||
@@ -173,11 +180,14 @@
|
||||
compactProducts?: boolean
|
||||
/** 编辑计划时排除本计划占用 */
|
||||
excludePlanId?: string
|
||||
/** 当前计划车牌(主牌),用于地磅可选车例外 */
|
||||
vehiclePlate?: string
|
||||
}>(),
|
||||
{
|
||||
readonly: false,
|
||||
compactProducts: true,
|
||||
excludePlanId: ''
|
||||
excludePlanId: '',
|
||||
vehiclePlate: ''
|
||||
}
|
||||
)
|
||||
|
||||
@@ -198,6 +208,9 @@
|
||||
const customerOptionsLoading = ref(false)
|
||||
|
||||
const isInternal = computed(() => isPlInternalCustomerName(props.modelValue.customerName))
|
||||
const canManualDibang = computed(() =>
|
||||
canManualToggleStopDibang(props.modelValue.customerName, props.vehiclePlate)
|
||||
)
|
||||
|
||||
const stopContractId = computed(() => {
|
||||
const id = String(props.modelValue.contracts?.[0]?.contractId || '').trim()
|
||||
@@ -365,7 +378,7 @@
|
||||
}
|
||||
const customer = customerOptions.value.find((item) => String(item.id) === String(customerId))
|
||||
const customerName = customer?.customerName || ''
|
||||
const defaults = resolveStopDibangDefaults(customerName, 'load')
|
||||
const defaults = resolveStopDibangDefaults(customerName, 'load', props.vehiclePlate)
|
||||
patch({
|
||||
customerId: String(customerId),
|
||||
customerName,
|
||||
@@ -403,9 +416,11 @@
|
||||
}
|
||||
|
||||
function handleDibangToggle(checked: boolean) {
|
||||
// 仅山峡/云栋可生成地磅单,且由客户类型自动决定,不允许手动勾选
|
||||
if (!isInternal.value) {
|
||||
patch({ generateDibang: 0, dibangDirection: 'none' })
|
||||
// 非可手动场景:外部客户强制不勾;内部非可选车由默认值锁定,忽略手动切换
|
||||
if (!canManualDibang.value) {
|
||||
if (!isInternal.value) {
|
||||
patch({ generateDibang: 0, dibangDirection: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
patch({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
title="从地址库选择"
|
||||
width="1200px"
|
||||
width="1320px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -74,6 +74,12 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="notes"
|
||||
label="注意事项"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import {
|
||||
canManualToggleStopDibang,
|
||||
resolveStopDibangDefaults
|
||||
} from './pl-plan-stop-form'
|
||||
import { isPlDibangOptionalVehiclePlate } from './pl-internal-customer'
|
||||
|
||||
const INTERNAL = '安庆市山峡化工有限公司'
|
||||
const YUNDONG = '安徽云栋化工有限公司'
|
||||
const EXTERNAL = '某某贸易有限公司'
|
||||
const OPTIONAL_PLATE = '皖H21522'
|
||||
|
||||
describe('isPlDibangOptionalVehiclePlate', () => {
|
||||
it('matches exact main plate', () => {
|
||||
assert.equal(isPlDibangOptionalVehiclePlate(OPTIONAL_PLATE), true)
|
||||
assert.equal(isPlDibangOptionalVehiclePlate(` ${OPTIONAL_PLATE} `), true)
|
||||
})
|
||||
|
||||
it('rejects other plates', () => {
|
||||
assert.equal(isPlDibangOptionalVehiclePlate('皖A12345'), false)
|
||||
assert.equal(isPlDibangOptionalVehiclePlate(''), false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveStopDibangDefaults + canManualToggleStopDibang', () => {
|
||||
it('internal + normal vehicle: auto-check, not manual', () => {
|
||||
assert.deepEqual(resolveStopDibangDefaults(INTERNAL, 'load', '皖A11111'), {
|
||||
generateDibang: 1,
|
||||
dibangDirection: 'outbound'
|
||||
})
|
||||
assert.deepEqual(resolveStopDibangDefaults(YUNDONG, 'delivery', '皖A11111'), {
|
||||
generateDibang: 1,
|
||||
dibangDirection: 'inbound'
|
||||
})
|
||||
assert.equal(canManualToggleStopDibang(INTERNAL, '皖A11111'), false)
|
||||
})
|
||||
|
||||
it('internal + optional vehicle: unchecked, manual allowed', () => {
|
||||
assert.deepEqual(resolveStopDibangDefaults(INTERNAL, 'load', OPTIONAL_PLATE), {
|
||||
generateDibang: 0,
|
||||
dibangDirection: 'none'
|
||||
})
|
||||
assert.deepEqual(resolveStopDibangDefaults(YUNDONG, 'delivery', OPTIONAL_PLATE), {
|
||||
generateDibang: 0,
|
||||
dibangDirection: 'none'
|
||||
})
|
||||
assert.equal(canManualToggleStopDibang(INTERNAL, OPTIONAL_PLATE), true)
|
||||
assert.equal(canManualToggleStopDibang(YUNDONG, OPTIONAL_PLATE), true)
|
||||
})
|
||||
|
||||
it('external + optional vehicle: still locked off', () => {
|
||||
assert.deepEqual(resolveStopDibangDefaults(EXTERNAL, 'load', OPTIONAL_PLATE), {
|
||||
generateDibang: 0,
|
||||
dibangDirection: 'none'
|
||||
})
|
||||
assert.equal(canManualToggleStopDibang(EXTERNAL, OPTIONAL_PLATE), false)
|
||||
})
|
||||
})
|
||||
@@ -4,11 +4,22 @@ export const PL_INTERNAL_CUSTOMER_NAMES = [
|
||||
'安徽云栋化工有限公司'
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 地磅单「可选」车辆主牌:内部客户时默认不勾且允许手动勾选。
|
||||
* 匹配主牌精确值(不含挂车拼接)。
|
||||
*/
|
||||
export const PL_DIBANG_OPTIONAL_VEHICLE_PLATES = ['皖H21522'] as const
|
||||
|
||||
export function isPlInternalCustomerName(name?: string | null): boolean {
|
||||
const n = (name || '').trim()
|
||||
return (PL_INTERNAL_CUSTOMER_NAMES as readonly string[]).includes(n)
|
||||
}
|
||||
|
||||
export function isPlDibangOptionalVehiclePlate(plate?: string | null): boolean {
|
||||
const p = (plate || '').trim()
|
||||
return (PL_DIBANG_OPTIONAL_VEHICLE_PLATES as readonly string[]).includes(p)
|
||||
}
|
||||
|
||||
/** 装货或送货段任一段客户为山峡/云栋时,需要现场链接。 */
|
||||
export function planRequiresSiteLink(
|
||||
plan?: {
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { Plan, PlanContractLink, PlanDelivery, PlanLoad, PlanStopProduct }
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import type { BrandOption } from '@/utils/crm-product-brand'
|
||||
import { formatBrandConstraintLabel } from '@/utils/crm-product-brand'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import {
|
||||
isPlDibangOptionalVehiclePlate,
|
||||
isPlInternalCustomerName
|
||||
} from '@/utils/pl-internal-customer'
|
||||
|
||||
export interface PlanStopProductFormRow {
|
||||
id?: string
|
||||
@@ -111,27 +114,45 @@ export interface PlanDeliveryForm {
|
||||
products: PlanStopProductFormRow[]
|
||||
}
|
||||
|
||||
/** 地磅勾选仅对内部客户(山峡/云栋)生效:默认勾选且不可改;其他客户默认不勾且不可勾。 */
|
||||
/**
|
||||
* 地磅勾选规则:
|
||||
* - 内部客户(山峡/云栋):默认勾选且不可改
|
||||
* - 地磅可选车(如皖H21522)+ 内部客户:默认不勾,可手动勾选
|
||||
* - 其他客户:默认不勾且不可勾
|
||||
*/
|
||||
export function resolveStopDibangDefaults(
|
||||
customerName?: string | null,
|
||||
stopType: 'load' | 'delivery' = 'load'
|
||||
stopType: 'load' | 'delivery' = 'load',
|
||||
vehiclePlate?: string | null
|
||||
): { generateDibang: number; dibangDirection: string } {
|
||||
if (!isPlInternalCustomerName(customerName)) {
|
||||
return { generateDibang: 0, dibangDirection: 'none' }
|
||||
}
|
||||
if (isPlDibangOptionalVehiclePlate(vehiclePlate)) {
|
||||
return { generateDibang: 0, dibangDirection: 'none' }
|
||||
}
|
||||
if (stopType === 'load') {
|
||||
return { generateDibang: 1, dibangDirection: 'outbound' }
|
||||
}
|
||||
return { generateDibang: 1, dibangDirection: 'inbound' }
|
||||
}
|
||||
|
||||
/** 仅「内部客户 + 地磅可选车」允许用户手动勾选地磅单 */
|
||||
export function canManualToggleStopDibang(
|
||||
customerName?: string | null,
|
||||
vehiclePlate?: string | null
|
||||
): boolean {
|
||||
return isPlInternalCustomerName(customerName) && isPlDibangOptionalVehiclePlate(vehiclePlate)
|
||||
}
|
||||
|
||||
function normalizeStopDibangFieldsFromApi(
|
||||
customerName: string | null | undefined,
|
||||
stopType: 'load' | 'delivery',
|
||||
generateDibang?: number | null,
|
||||
dibangDirection?: string | null
|
||||
dibangDirection?: string | null,
|
||||
vehiclePlate?: string | null
|
||||
): { generateDibang: number; dibangDirection: string } {
|
||||
const defaults = resolveStopDibangDefaults(customerName, stopType)
|
||||
const defaults = resolveStopDibangDefaults(customerName, stopType, vehiclePlate)
|
||||
// 非山峡/云栋:禁止生成地磅单(编辑回填也强制清掉历史勾选)
|
||||
if (!isPlInternalCustomerName(customerName)) {
|
||||
return { generateDibang: 0, dibangDirection: 'none' }
|
||||
@@ -140,7 +161,8 @@ function normalizeStopDibangFieldsFromApi(
|
||||
let direction = (dibangDirection || '').trim()
|
||||
if (gen) {
|
||||
if (direction !== 'outbound' && direction !== 'inbound') {
|
||||
direction = defaults.dibangDirection
|
||||
// 可选车默认 direction 为 none,勾选回填时按段类型补方向
|
||||
direction = stopType === 'load' ? 'outbound' : 'inbound'
|
||||
}
|
||||
} else {
|
||||
direction = 'none'
|
||||
@@ -409,13 +431,17 @@ export function resolveDeliveryViewAddress(
|
||||
return ''
|
||||
}
|
||||
|
||||
export function mapLoadFromApi(load?: PlanLoad | null): PlanLoadForm {
|
||||
export function mapLoadFromApi(
|
||||
load?: PlanLoad | null,
|
||||
vehiclePlate?: string | null
|
||||
): PlanLoadForm {
|
||||
if (!load) return createEmptyLoadForm()
|
||||
const dibang = normalizeStopDibangFieldsFromApi(
|
||||
load.customerName,
|
||||
'load',
|
||||
load.generateDibang,
|
||||
load.dibangDirection
|
||||
load.dibangDirection,
|
||||
vehiclePlate
|
||||
)
|
||||
return {
|
||||
customerId: load.customerId || '',
|
||||
@@ -436,14 +462,18 @@ export function mapLoadFromApi(load?: PlanLoad | null): PlanLoadForm {
|
||||
}
|
||||
}
|
||||
|
||||
export function mapDeliveriesFromApi(deliveries?: PlanDelivery[]): PlanDeliveryForm[] {
|
||||
export function mapDeliveriesFromApi(
|
||||
deliveries?: PlanDelivery[],
|
||||
vehiclePlate?: string | null
|
||||
): PlanDeliveryForm[] {
|
||||
if (!deliveries?.length) return [createEmptyDeliveryForm(1)]
|
||||
return deliveries.map((row, index) => {
|
||||
const dibang = normalizeStopDibangFieldsFromApi(
|
||||
row.customerName,
|
||||
'delivery',
|
||||
row.generateDibang,
|
||||
row.dibangDirection
|
||||
row.dibangDirection,
|
||||
vehiclePlate
|
||||
)
|
||||
return {
|
||||
seq: row.seq ?? index + 1,
|
||||
@@ -471,8 +501,9 @@ export function mapLegacyPlanToStops(plan: Plan): {
|
||||
load: PlanLoadForm
|
||||
deliveries: PlanDeliveryForm[]
|
||||
} {
|
||||
const loadDefaults = resolveStopDibangDefaults(plan.supplierName, 'load')
|
||||
const deliveryDefaults = resolveStopDibangDefaults(plan.customerName, 'delivery')
|
||||
const plate = plan.licensePlate
|
||||
const loadDefaults = resolveStopDibangDefaults(plan.supplierName, 'load', plate)
|
||||
const deliveryDefaults = resolveStopDibangDefaults(plan.customerName, 'delivery', plate)
|
||||
const stopProducts = createStopProductsFromPlanProducts(
|
||||
(plan.products || []).map((item, index) => ({
|
||||
id: item.id,
|
||||
|
||||
Reference in new issue
Block a user