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

@@ -7,13 +7,16 @@
'white': #ffffff,
'black': #000000,
'success': (
'base': #168f45
'base': #059669
),
'warning': (
'base': #ffae1f
'base': #d97706
),
'danger': (
'base': #ff4d4f
'base': #dc2626
),
'info': (
'base': #1e293b
),
'error': (
'base': #fa896b
+55 -44
View File
@@ -10,6 +10,11 @@
--el-color-primary-light-8: #c8ecd4 !important;
--el-color-primary-light-9: var(--mint-primary-bg) !important;
--el-color-primary-dark-2: var(--mint-primary-dark) !important;
// 语义色:供 el-tag effect=dark 使用实心底(避免 Element 默认 info #909399)
--el-color-success: #059669 !important;
--el-color-warning: #d97706 !important;
--el-color-danger: #dc2626 !important;
--el-color-info: #1e293b !important;
--el-border-color: var(--mint-border) !important;
--el-border-color-light: #dfece6 !important;
--el-fill-color-light: var(--mint-table-head) !important;
@@ -302,20 +307,67 @@ html.dark .el-card {
}
}
// 修改el-tag样式
.el-tag {
font-weight: 500;
// 语义 Tag:深色底 + 白字(配合全局 ElTag 默认 effect=dark;覆盖 light 残留)
html body .el-tag {
border: none !important;
border-radius: 6px;
padding: 2px 8px;
font-size: 12px;
font-weight: 600;
transition: all 0s !important;
--el-tag-text-color: #ffffff;
color: #ffffff !important;
&.el-tag--default {
height: 26px !important;
}
.el-tag__close {
color: rgba(255, 255, 255, 0.85) !important;
&:hover {
background-color: rgba(255, 255, 255, 0.2) !important;
color: #ffffff !important;
}
}
&,
&.el-tag--primary,
&.el-tag--success {
--el-tag-bg-color: #059669;
--el-tag-border-color: #059669;
--el-tag-hover-color: #047857;
background: #059669 !important;
background-color: #059669 !important;
border-color: #059669 !important;
}
&.el-tag--warning {
--el-tag-bg-color: #d97706;
--el-tag-border-color: #d97706;
--el-tag-hover-color: #b45309;
background: #d97706 !important;
background-color: #d97706 !important;
border-color: #d97706 !important;
}
&.el-tag--danger {
--el-tag-bg-color: #dc2626;
--el-tag-border-color: #dc2626;
--el-tag-hover-color: #b91c1c;
background: #dc2626 !important;
background-color: #dc2626 !important;
border-color: #dc2626 !important;
}
// 中性 info:无背景,正文标准色(与彩色语义 Tag 区分)
&.el-tag--info {
--el-tag-bg-color: transparent;
--el-tag-border-color: transparent;
--el-tag-text-color: var(--mint-text);
--el-tag-hover-color: var(--mint-text);
background: transparent !important;
background-color: transparent !important;
border-color: transparent !important;
color: var(--mint-text) !important;
}
@@ -858,47 +910,6 @@ body .el-table thead {
white-space: nowrap;
}
// Fresh Mint:状态标签(映射 design spec 的浅底 + 语义色字)
.page-container .el-tag,
.console-container .el-tag {
border: none;
border-radius: 6px;
padding: 2px 8px;
font-size: 12px;
font-weight: 600;
}
.page-container .el-tag--success,
.console-container .el-tag--success {
background: #d4f0de !important;
color: #147a3c !important;
}
.page-container .el-tag--warning,
.console-container .el-tag--warning {
background: #fffbeb !important;
color: #d97706 !important;
}
.page-container .el-tag--danger,
.console-container .el-tag--danger {
background: #fef2f2 !important;
color: #dc2626 !important;
}
.page-container .el-tag--info,
.console-container .el-tag--info {
background: transparent !important;
border-color: transparent !important;
color: var(--mint-text) !important;
}
.page-container .el-tag--primary,
.console-container .el-tag--primary {
background: #e6f8ed !important;
color: #168f45 !important;
}
.page-container .el-button--primary,
.console-container .el-button--primary {
--el-button-bg-color: var(--mint-primary);
@@ -46,8 +46,8 @@
--warning-bg: #fffbeb;
--danger: #dc2626;
--danger-bg: #fef2f2;
--info: #64748b;
--info-bg: #f8fafc;
--info: #1e293b;
--info-bg: #f1f5f9;
/* 弹窗相对视口上下留白(各 20px,总 40px) */
--dialog-viewport-inset: 20px;
@@ -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
}
+9 -3
View File
@@ -1,8 +1,12 @@
/**
* 全局包装 ElTag:默认插槽无有效内容时不渲染,避免空值时出现带背景色的空 tag。
* 通过 vite Components 自定义 resolver 替换自动导入的 ElTag。
* 全局包装 ElTag:
* 1) 默认插槽无有效内容时不渲染,避免空值时出现带背景色的空 tag
* 2) 默认 effect="dark"(深色底 + 白字),覆盖 Element Plus 默认 light(浅底彩字 / info=#909399)
* 通过 vite Components 自定义 resolver + main.ts 全局注册替换 ElTag。
*/
import { ElTag } from 'element-plus'
// 走 SCSS 源码,使 el-light.scss 中的语义色生效(info Tag 另由 el-ui 设为无背景+正文色)
import 'element-plus/es/components/tag/style/index'
import { Comment, Text, defineComponent, h, type VNode, type VNodeArrayChildren } from 'vue'
function hasVisibleDefaultContent(vnodes: VNode[] | undefined): boolean {
@@ -39,7 +43,9 @@ const EmptyAwareElTag = defineComponent({
if (!hasVisibleDefaultContent(slots.default?.())) {
return null
}
return h(ElTag, { ...props, ...attrs }, slots)
// ElTag 自身 default effect 为 light(浅底彩字);此处一律强制 dark(深色底白字)
// 注意:不能读 props.effect 再 ?? 'dark',因默认值已是 'light',永远不会落到 dark
return h(ElTag, { ...props, ...attrs, effect: 'dark' }, slots)
}
}
})
+3 -3
View File
@@ -22,7 +22,7 @@ export function isRequiredCertSlotComplete(
if (!cert) return false
if (!matchRequiredCertName(cert.certName, requiredName)) return false
if (!cert.expireDate) return false
if (!cert.attachmentUrls?.trim()) return false
// 证书附件非必填
return true
}
@@ -31,11 +31,11 @@ export function getMissingRequiredCertFieldLabels(
requiredName: string
): string[] {
if (!cert || !matchRequiredCertName(cert.certName, requiredName)) {
return ['有效期', '附件']
return ['有效期']
}
const missing: string[] = []
if (!cert.expireDate) missing.push('有效期')
if (!cert.attachmentUrls?.trim()) missing.push('附件')
// 证书附件非必填
return missing
}
+16 -9
View File
@@ -1,16 +1,23 @@
/**
* 三方车辆/人员客户文本粘贴解析
* 示例:
* 皖LB1405 皖LP359
* 核载:34 吨
* 驾驶员:徐孝军
* 手机号:18956652137
* 身份证:342921198602020031
* 押运员:汪海明
* 电话:13955522828
* 身份证:342829197009084019
* 标准格式见 `PL_PASTE_SAMPLE_TEXT`。
*/
/**
* 粘贴识别输入框预填示例(格式与解析规则一致,敏感字段已脱敏,供用户对照改写)
* 姓名保留姓、手机保留前 3 后 4、身份证保留前 6 后 4、车牌保留省份简称后打码
*/
export const PL_PASTE_SAMPLE_TEXT = [
'皖****** 皖******',
'核载:34 吨',
'驾驶员:张**',
'手机号:138****0000',
'身份证:340000********0000',
'押运员:李**',
'电话:139****0000',
'身份证:340000********0000'
].join('\n')
export interface PlPastePersonnelDraft {
personnelName: string
phoneNo?: string
+4 -8
View File
@@ -119,7 +119,7 @@ export function isPersonnelCertStrictComplete(
if (!cert?.certName?.trim()) return false
if (!matchRequiredCertName(cert.certName, requiredName)) return false
if (!cert.expireDate) return false
if (!cert.attachmentUrls?.trim()) return false
// 证书附件非必填
return true
}
@@ -136,11 +136,7 @@ export function matchRequiredCertName(
}
export function isPersonnelCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
return Boolean(
cert?.certName?.trim() &&
cert?.expireDate &&
cert?.attachmentUrls?.trim()
)
return Boolean(cert?.certName?.trim() && cert?.expireDate)
}
export function getRequiredPersonnelCertNames(role: PersonnelCertRole): readonly string[] {
@@ -215,11 +211,11 @@ function getPersonnelCertMissingFieldLabels(
requiredName: string
): string[] {
if (!cert || !matchRequiredCertName(cert.certName, requiredName)) {
return ['有效期', '附件']
return ['有效期']
}
const missing: string[] = []
if (!cert.expireDate) missing.push('有效期')
if (!cert.attachmentUrls?.trim()) missing.push('附件')
// 证书附件非必填
return missing
}
+3 -7
View File
@@ -77,11 +77,7 @@ export function mergeVehicleCertsByPart<T extends CertRow>(
}
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
return Boolean(
cert?.certName?.trim() &&
cert?.expireDate &&
cert?.attachmentUrls?.trim()
)
return Boolean(cert?.certName?.trim() && cert?.expireDate)
}
export function getVehicleHeadRequiredCertNames(
@@ -263,11 +259,11 @@ function getVehicleCertMissingFieldLabels(
requiredName: string
): string[] {
if (!cert || !matchVehicleRequiredCertName(cert.certName, requiredName)) {
return ['有效期', '附件']
return ['有效期']
}
const missing: string[] = []
if (!cert.expireDate) missing.push('有效期')
if (!cert.attachmentUrls?.trim()) missing.push('附件')
// 证书附件非必填
return missing
}
+46 -3
View File
@@ -189,9 +189,49 @@
min-width="180"
show-overflow-tooltip
/>
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
<el-table-column
v-if="isColumnVisible('productName')"
label="产品名称"
min-width="200"
align="left"
header-align="left"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
<PlPlanListProductCell :products="row.products" field="name" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productSpec')"
label="型号/规格"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="spec" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productQty')"
label="数量"
min-width="90"
align="right"
header-align="right"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="qty" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productUnit')"
label="单位"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="unit" />
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="200">
@@ -328,7 +368,10 @@ const listColumnDefs = [
{ prop: 'followStatus', label: '跟进状态' },
{ prop: 'planType', label: '单据类型' },
{ prop: 'customerName', label: '客户名称' },
{ prop: 'products', label: '产品信息' },
{ prop: 'productName', label: '产品名称' },
{ prop: 'productSpec', label: '型号/规格' },
{ prop: 'productQty', label: '数量' },
{ prop: 'productUnit', label: '单位' },
{ prop: 'planAttachments', label: '计划附件' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'personnelSummary', label: '人员' },
+46 -3
View File
@@ -179,9 +179,49 @@
min-width="120"
align="center"
/>
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
<el-table-column
v-if="isColumnVisible('productName')"
label="产品名称"
min-width="200"
align="left"
header-align="left"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
<PlPlanListProductCell :products="row.products" field="name" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productSpec')"
label="型号/规格"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="spec" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productQty')"
label="数量"
min-width="90"
align="right"
header-align="right"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="qty" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productUnit')"
label="单位"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="unit" />
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="280">
@@ -382,7 +422,10 @@ const planList = ref<Plan[]>([])
const listColumnDefs = [
{ prop: 'planDate', label: '计划日期' },
{ prop: 'products', label: '产品信息' },
{ prop: 'productName', label: '产品名称' },
{ prop: 'productSpec', label: '型号/规格' },
{ prop: 'productQty', label: '数量' },
{ prop: 'productUnit', label: '单位' },
{ prop: 'planAttachments', label: '计划附件' },
{ prop: 'customerName', label: '客户名称' },
{ prop: 'planType', label: '单据类型' },
+42 -5
View File
@@ -246,14 +246,48 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('products')"
label="产品信息"
min-width="240"
v-if="isColumnVisible('productName')"
label="产品名称"
min-width="200"
align="left"
header-align="left"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
<PlPlanListProductCell :products="row.products" field="name" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productSpec')"
label="型号/规格"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="spec" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productQty')"
label="数量"
min-width="90"
align="right"
header-align="right"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="qty" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('productUnit')"
label="单位"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" field="unit" />
</template>
</el-table-column>
<el-table-column
@@ -490,7 +524,10 @@ const listColumnDefs = [
{ prop: 'planDate', label: '计划日期' },
{ prop: 'planType', label: '单据类型' },
{ prop: 'customerName', label: '客户名称' },
{ prop: 'products', label: '产品信息' },
{ prop: 'productName', label: '产品名称' },
{ prop: 'productSpec', label: '型号/规格' },
{ prop: 'productQty', label: '数量' },
{ prop: 'productUnit', label: '单位' },
{ prop: 'planAttachments', label: '计划附件' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'personnelSummary', label: '人员' },
+26 -13
View File
@@ -441,6 +441,7 @@ import {
type VehicleDetail,
type VehiclePersonnelRow,
type CertRow,
type CertRead,
type Personnel,
type Carrier
} from '@/api/pl'
@@ -454,7 +455,7 @@ import SystemViewDialog, {
} from '@/components/business/system-view-dialog/index.vue'
import { loadPersonnelWithCerts } from '@/composables/usePlVehicleViewDetail'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { pickBxList, pickBxPageResult } from '@/utils/http/bx'
import { pickBxData, pickBxList, pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import { formatDateTime } from '@/utils/format'
import {
@@ -488,6 +489,7 @@ type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
const loading = ref(false)
const quickSearchKeyword = ref('')
const submitLoading = ref(false)
const dialogLoading = ref(false)
const vehicleList = ref<Vehicle[]>([])
const carrierList = ref<Carrier[]>([])
@@ -554,7 +556,6 @@ const formData = reactive({
const formRules = {
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
trailerNo: [{ required: true, message: '请输入挂车号', trigger: 'blur' }],
vehicleStatus: [{ required: true, message: '请选择车辆状态', trigger: 'change' }],
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }]
}
@@ -923,14 +924,28 @@ async function handleView(row: Vehicle) {
}
}
function mapCertRowsForForm(certs: CertRead[] | undefined | null) {
return (certs || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
vehiclePart: c.vehiclePart,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate ? String(c.expireDate).slice(0, 10) : '',
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
}))
}
async function handleEdit(row: Vehicle) {
dialogTitle.value = '编辑自有车辆'
isEdit.value = true
dialogLoading.value = true
dialogVisible.value = true
try {
await loadPersonnelOptions()
const res = await vehicleApi.get(row.id)
const detail = ((res.data ?? res) || {}) as VehicleDetail
const detail = pickBxData<VehicleDetail>(res) || ({} as VehicleDetail)
if (detail.vehicleType !== VEHICLE_TYPE_SELF) {
ElMessage.warning('仅可编辑自有车辆')
dialogVisible.value = false
@@ -953,23 +968,21 @@ async function handleEdit(row: Vehicle) {
age: p.age ?? null
}))
)
// 等 trailerNo 相关 watch 先消化,再回填证书,避免被 hasTrailer 逻辑误清空
await nextTick()
const sourceCerts =
detail.certList?.length ? detail.certList : row.certList?.length ? row.certList : []
certList.value = ensureSelfVehicleCertSlots(
(detail.certList || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
vehiclePart: c.vehiclePart,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate,
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
})),
mapCertRowsForForm(sourceCerts),
formData.trailerNo
)
await nextTick()
certFormListRef.value?.refreshRequiredSlots?.()
} catch {
dialogVisible.value = false
ElMessage.error('加载车辆详情失败')
} finally {
dialogLoading.value = false
}
}
+31 -15
View File
@@ -459,6 +459,7 @@ import {
type VehicleDetail,
type VehiclePersonnelRow,
type CertRow,
type CertRead,
type Carrier,
type Personnel
} from '@/api/pl'
@@ -472,7 +473,7 @@ import SystemViewDialog, {
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { pickBxList, pickBxPageResult } from '@/utils/http/bx'
import { pickBxData, pickBxList, pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import { formatDateTime } from '@/utils/format'
import { formatPlVehiclePlateDisplay, normalizePlPersonnelSummaryDisplay } from '@/utils/pl-display'
@@ -498,6 +499,8 @@ const loading = ref(false)
const quickSearchKeyword = ref('')
const submitLoading = ref(false)
const dialogLoading = ref(false)
/** 编辑回填中:跳过 vehicleType watch,避免证书被 ensure 覆盖清空 */
const hydratingEdit = ref(false)
const vehicleList = ref<Vehicle[]>([])
const carrierList = ref<Carrier[]>([])
@@ -567,7 +570,6 @@ const formData = reactive({
const formRules = {
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
trailerNo: [{ required: true, message: '请输入挂车号', trigger: 'blur' }],
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }]
}
@@ -974,15 +976,29 @@ async function handleView(row: Vehicle) {
}
}
function mapCertRowsForForm(certs: CertRead[] | undefined | null) {
return (certs || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
vehiclePart: c.vehiclePart,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate ? String(c.expireDate).slice(0, 10) : '',
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
}))
}
async function handleEdit(row: Vehicle) {
dialogTitle.value = '编辑车辆'
isEdit.value = true
dialogLoading.value = true
hydratingEdit.value = true
dialogVisible.value = true
try {
await loadPersonnelOptions()
const res = await vehicleApi.get(row.id)
const detail = ((res.data ?? res) || {}) as VehicleDetail
const detail = pickBxData<VehicleDetail>(res) || ({} as VehicleDetail)
Object.assign(formData, {
id: detail.id,
licensePlate: detail.licensePlate,
@@ -1001,19 +1017,19 @@ async function handleEdit(row: Vehicle) {
age: p.age ?? null
}))
)
const mappedCerts = (detail.certList || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
vehiclePart: c.vehiclePart,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate,
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
}))
certList.value = ensureVehicleCertSlots(mappedCerts, formData.trailerNo, VEHICLE_TYPE_LOCK)
// 等 trailerNo 相关 watch 先消化,再回填证书,避免被 hasTrailer 逻辑误清空
await nextTick()
const sourceCerts =
detail.certList?.length ? detail.certList : row.certList?.length ? row.certList : []
certList.value = ensureVehicleCertSlots(
mapCertRowsForForm(sourceCerts),
formData.trailerNo,
VEHICLE_TYPE_LOCK
)
await nextTick()
certFormListRef.value?.refreshRequiredSlots?.()
} finally {
hydratingEdit.value = false
dialogLoading.value = false
}
}
@@ -1105,7 +1121,7 @@ async function handleDelete(row: Vehicle) {
watch(
() => formData.vehicleType,
async type => {
if (!dialogVisible.value) return
if (!dialogVisible.value || hydratingEdit.value) return
await loadPersonnelOptions()
if (type === 'self' || type === 'carrier') {
certList.value = ensureVehicleCertSlots(