This commit is contained in:
zlwu committed 2026-09-03 13:50:25 +08:00
1 parent 67d37a1f0e
commit 54a1ba0c5c
21 files changed
+8646 -7577

No files matched your search

-1
View File
@@ -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` 为准)
- 不扩展其他车牌(后续可加常量列表)
- 不改列表/查看态展示逻辑(只读展示已存字段)
+6 -6
View File
@@ -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": {
+3 -3
View File
@@ -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
View File
@@ -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
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?: {
+43 -12
View File
@@ -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,