This commit is contained in:
zlwu committed 2026-08-02 17:18:33 +08:00
1 parent c00f19f8bd
commit 272c68af4b
134 files changed
+930389 -38617

No files matched your search

@@ -3,7 +3,7 @@
<el-dialog
v-model="innerVisible"
:title="dialogTitle"
width="820px"
width="1100px"
align-center
destroy-on-close
append-to-body
@@ -13,83 +13,84 @@
style="backdrop-filter: none"
@closed="handleClosed"
>
<el-form label-position="top" class="form-grid-2">
<el-form-item label="客户原文" class="form-col-span-2">
<el-form label-position="top">
<el-form-item label="客户原文">
<el-input
v-model="rawText"
type="textarea"
:rows="8"
placeholder="粘贴客户提供的车辆与驾押信息后点击识别"
:rows="10"
placeholder="请按示例格式填写或粘贴客户原文后点击识别"
/>
</el-form-item>
<el-form-item class="form-col-span-2">
<el-form-item>
<el-button type="primary" @click="handleParse">
<el-icon><MagicStick /></el-icon>识别
</el-button>
</el-form-item>
<template v-if="parsed">
<el-form-item v-if="showVehicleFields" label="承运商" class="form-col-span-2" required>
<el-select
v-model="carrierId"
placeholder="请选择承运商"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="c in carrierOptions"
:key="c.id"
:label="c.carrierName"
:value="c.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="mode === 'personnel-first' && hasPlate"
label="建车选项"
class="form-col-span-2"
>
<el-form-item v-if="mode === 'personnel-first' && hasPlate" label="建车选项">
<el-checkbox v-model="createVehicle">一并创建三方车辆并关联</el-checkbox>
</el-form-item>
<el-form-item v-if="showVehicleFields" label="主车号">
<el-input v-model="vehicleForm.licensePlate" maxlength="20" />
</el-form-item>
<el-form-item v-if="showVehicleFields" label="挂车号">
<el-input v-model="vehicleForm.trailerNo" maxlength="20" clearable />
</el-form-item>
<el-form-item v-if="showVehicleFields" label="核载(吨)">
<el-input-number
v-model="vehicleForm.loadCapacity"
:min="0"
:precision="2"
style="width: 100%"
/>
</el-form-item>
<div v-if="showVehicleFields" class="form-grid-4">
<el-form-item label="承运商" required>
<el-select
v-model="carrierId"
placeholder="请选择承运商"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="c in carrierOptions"
:key="c.id"
:label="c.carrierName"
:value="c.id"
/>
</el-select>
</el-form-item>
<el-form-item label="主车号">
<el-input v-model="vehicleForm.licensePlate" maxlength="20" />
</el-form-item>
<el-form-item label="挂车号">
<el-input v-model="vehicleForm.trailerNo" maxlength="20" clearable />
</el-form-item>
<el-form-item label="核载(吨)">
<el-input-number
v-model="vehicleForm.loadCapacity"
:min="0"
:precision="2"
style="width: 100%"
/>
</el-form-item>
</div>
<el-form-item label="驾驶员">
<el-input v-model="driverForm.personnelName" maxlength="100" />
</el-form-item>
<el-form-item label="驾驶员手机">
<el-input v-model="driverForm.phoneNo" maxlength="32" clearable />
</el-form-item>
<el-form-item label="驾驶员身份证" class="form-col-span-2">
<el-input v-model="driverForm.idCardNo" maxlength="32" clearable />
</el-form-item>
<div class="form-grid-3">
<el-form-item label="驾驶员">
<el-input v-model="driverForm.personnelName" maxlength="100" />
</el-form-item>
<el-form-item label="驾驶员手机">
<el-input v-model="driverForm.phoneNo" maxlength="32" clearable />
</el-form-item>
<el-form-item label="驾驶员身份证">
<el-input v-model="driverForm.idCardNo" maxlength="32" clearable />
</el-form-item>
</div>
<el-form-item label="押运员">
<el-input v-model="escortForm.personnelName" maxlength="100" />
</el-form-item>
<el-form-item label="押运员电话">
<el-input v-model="escortForm.phoneNo" maxlength="32" clearable />
</el-form-item>
<el-form-item label="押运员身份证" class="form-col-span-2">
<el-input v-model="escortForm.idCardNo" maxlength="32" clearable />
</el-form-item>
<div class="form-grid-3">
<el-form-item label="押运员">
<el-input v-model="escortForm.personnelName" maxlength="100" />
</el-form-item>
<el-form-item label="押运员电话">
<el-input v-model="escortForm.phoneNo" maxlength="32" clearable />
</el-form-item>
<el-form-item label="押运员身份证">
<el-input v-model="escortForm.idCardNo" maxlength="32" clearable />
</el-form-item>
</div>
<div v-if="conflictRows.length" class="form-col-span-2 paste-conflict-block">
<div v-if="conflictRows.length" class="paste-conflict-block">
<h4 class="paste-conflict-title">冲突处理</h4>
<div v-for="row in conflictRows" :key="row.key" class="paste-conflict-row">
<div class="paste-conflict-label">
@@ -105,7 +106,6 @@
<el-alert
v-if="parsed.warnings.length"
class="form-col-span-2"
type="warning"
:closable="false"
:title="`有 ${parsed.warnings.length} 行未识别`"
@@ -140,6 +140,7 @@
} from '@/api/pl'
import { pickBxPageResult } from '@/utils/http/bx'
import {
PL_PASTE_SAMPLE_TEXT,
parsePlCustomerVehicleText,
type PlPasteRecognizeResult
} from '@/utils/pl-paste-recognize'
@@ -169,7 +170,7 @@
props.mode === 'vehicle-bundle' ? '粘贴识别(整车套装)' : '粘贴识别(以人为主)'
)
const rawText = ref('')
const rawText = ref(PL_PASTE_SAMPLE_TEXT)
const parsed = ref<PlPasteRecognizeResult | null>(null)
const carrierId = ref('')
const createVehicle = ref(true)
@@ -376,7 +377,7 @@
}
function handleClosed() {
rawText.value = ''
rawText.value = PL_PASTE_SAMPLE_TEXT
parsed.value = null
carrierId.value = ''
createVehicle.value = true
@@ -395,12 +396,20 @@
watch(
() => props.visible,
async v => {
if (v) await ensureCarriers()
if (v) {
rawText.value = PL_PASTE_SAMPLE_TEXT
await ensureCarriers()
}
}
)
</script>
<style scoped lang="scss">
.paste-conflict-block,
:deep(.el-alert) {
margin-top: 16px;
}
.paste-conflict-block {
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
@@ -5,33 +5,72 @@
:key="product.id || `${product.productNo}-${index}`"
class="pl-plan-list-product-cell__row"
>
<span class="pl-plan-list-product-cell__line">
<span class="pl-plan-list-product-cell__text">{{ formatProductLine(product) }}</span>
<el-tag v-if="getHazardFlags(product).isToxic" type="warning" size="small" class="pl-plan-list-product-cell__mark">
毒
</el-tag>
<el-tag
v-if="getHazardFlags(product).isExplosive"
type="danger"
size="small"
class="pl-plan-list-product-cell__mark"
>
爆
</el-tag>
<span
v-if="showCertStatus && isHazardProduct(product)"
class="pl-plan-list-product-cell__certs"
>
<span
v-for="slot in getRequiredCertSlots(product)"
:key="slot.key"
class="pl-plan-list-product-cell__cert-text"
<span
class="pl-plan-list-product-cell__line"
:class="{
'pl-plan-list-product-cell__line--qty': field === 'qty',
'pl-plan-list-product-cell__line--center': field === 'spec' || field === 'unit'
}"
>
<template v-if="!field">
<span class="pl-plan-list-product-cell__text">{{ formatProductLine(product) }}</span>
<el-tag
v-if="getHazardFlags(product).isToxic"
type="warning"
size="small"
class="pl-plan-list-product-cell__mark"
>
{{ slot.label }}(<span
:class="{ 'pl-plan-list-product-cell__cert-status--missing': !isProductCertSlotFilled(product, slot.key) }"
>{{ isProductCertSlotFilled(product, slot.key) ? '有' : '无' }}</span>)
毒
</el-tag>
<el-tag
v-if="getHazardFlags(product).isExplosive"
type="danger"
size="small"
class="pl-plan-list-product-cell__mark"
>
爆
</el-tag>
<span
v-if="showCertStatus && isHazardProduct(product)"
class="pl-plan-list-product-cell__certs"
>
<span
v-for="slot in getRequiredCertSlots(product)"
:key="slot.key"
class="pl-plan-list-product-cell__cert-text"
>
{{ slot.label }}(<span
:class="{
'pl-plan-list-product-cell__cert-status--missing': !isProductCertSlotFilled(
product,
slot.key
)
}"
>{{ isProductCertSlotFilled(product, slot.key) ? '有' : '无' }}</span
>)
</span>
</span>
</span>
</template>
<template v-else-if="field === 'name'">
<span class="pl-plan-list-product-cell__text">{{ product.productName || '—' }}</span>
<el-tag
v-if="getHazardFlags(product).isToxic"
type="warning"
size="small"
class="pl-plan-list-product-cell__mark"
>
毒
</el-tag>
<el-tag
v-if="getHazardFlags(product).isExplosive"
type="danger"
size="small"
class="pl-plan-list-product-cell__mark"
>
爆
</el-tag>
</template>
<span v-else class="pl-plan-list-product-cell__text">{{ formatFieldValue(product) }}</span>
</span>
</div>
</div>
@@ -47,12 +86,20 @@ import {
resolveProductHazardFlags
} from '@/composables/usePlToxicProductCertCheck'
import { useDictStore } from '@/store/modules/dict'
import { formatPlanProductListText } from '@/utils/pl-display'
import {
formatPlanProductListText,
getPlanProductQuantity,
getPlanProductSpec
} from '@/utils/pl-display'
withDefaults(
export type PlPlanListProductCellField = 'name' | 'spec' | 'qty' | 'unit'
const props = withDefaults(
defineProps<{
products?: PlanProduct[]
/** 危化品行下方展示证书有无(按产品类型) */
/** 按字段拆列展示;未传时保持整行拼接 */
field?: PlPlanListProductCellField
/** 危化品行下方展示证书有无(按产品类型;仅整行模式) */
showCertStatus?: boolean
}>(),
{
@@ -71,6 +118,21 @@ function formatProductLine(product: PlanProduct) {
return formatPlanProductListText(product, getUnitLabel)
}
function formatFieldValue(product: PlanProduct) {
if (props.field === 'spec') {
return getPlanProductSpec(product)
}
if (props.field === 'qty') {
return getPlanProductQuantity(product)
}
if (props.field === 'unit') {
const unitRaw = product.unit || ''
if (!unitRaw) return '—'
return getUnitLabel(unitRaw) || unitRaw
}
return '—'
}
function getHazardFlags(product: PlanProduct) {
return resolveProductHazardFlags(product)
}
@@ -105,6 +167,18 @@ function getHazardFlags(product: PlanProduct) {
text-align: left;
}
.pl-plan-list-product-cell__line--qty {
justify-content: flex-end;
text-align: right;
width: 100%;
}
.pl-plan-list-product-cell__line--center {
justify-content: center;
text-align: center;
width: 100%;
}
.pl-plan-list-product-cell__text {
min-width: 0;
}
@@ -1224,7 +1224,17 @@
mapContractProducts(contractSnapshot.value?.products)
)
const hazardProducts = computed(() => products.value.filter((item) => isHazardProduct(item)))
/** 危化证区域展示 CRM 标准名(productRows 已回填 standardProductName;库内 productName 可能是别名) */
const hazardProducts = computed(() =>
products.value
.filter((item) => isHazardProduct(item))
.map((item) => {
const formRow = item.id ? planProductFormById.value.get(String(item.id)) : undefined
const standardName = String(formRow?.standardProductName || '').trim()
if (!standardName || standardName === item.productName) return item
return { ...item, productName: standardName }
})
)
async function handleViewContract() {
const contractId = contractSnapshot.value?.id || plan.value?.contractId
@@ -125,8 +125,10 @@ function syncCertList(headCerts: CertRowWithKey[], trailerCerts: CertRowWithKey[
certList.value = mergeVehicleCertsByPart(headCerts, trailerCerts, hasTrailer.value)
}
watch(hasTrailer, value => {
if (!value) {
watch(hasTrailer, (value, oldValue) => {
// 仅在「有挂车 → 无挂车」时剥离挂车证书,避免编辑回填时
// trailerNo 与 certList 同 tick 赋值触发误清空。
if (!value && oldValue) {
activePart.value = PL_VEHICLE_CERT_PART_HEAD
certList.value = mergeVehicleCertsByPart(
splitVehicleCertsByPart(certList.value).headCerts,
@@ -137,6 +139,10 @@ watch(hasTrailer, value => {
trailerCertFormRef.value?.clearUploadRefs()
return
}
if (!value) {
activePart.value = PL_VEHICLE_CERT_PART_HEAD
return
}
if (activePart.value !== PL_VEHICLE_CERT_PART_TRAILER) {
activePart.value = PL_VEHICLE_CERT_PART_HEAD
}