提交
This commit is contained in:
1 parent
cedb7ee681
commit
c90f72fd34
22 files changed
+5163
-3825
No files matched your search
@@ -15,7 +15,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="categoryName" label="产品分类" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="税率(%)" min-width="90" align="center">
|
||||
<template #default="{ row }">{{ row.taxRate ?? 0 }}%</template>
|
||||
<template #default="{ row }">
|
||||
{{ row.taxRate != null ? `${row.taxRate}%` : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" min-width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -35,7 +37,7 @@
|
||||
<span v-else>{{ row.quantity ?? '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际数量" min-width="110" align="right">
|
||||
<el-table-column v-if="showActualQty" label="实际数量" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="editableQty"
|
||||
@@ -94,6 +96,8 @@ defineProps<{
|
||||
showDelete?: boolean
|
||||
editableRemark?: boolean
|
||||
editableQty?: boolean
|
||||
/** 新增/编辑弹窗默认不展示实际数量 */
|
||||
showActualQty?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -51,54 +51,26 @@
|
||||
<section class="pl-plan-form-section">
|
||||
<div class="pl-plan-form-section-head">
|
||||
<h3 class="pl-plan-form-section-title">合同信息</h3>
|
||||
<el-button class="btn-main" type="primary" @click="contractSelectorVisible = true">
|
||||
<el-icon><Search /></el-icon>选择合同
|
||||
</el-button>
|
||||
<div class="pl-plan-form-section-actions">
|
||||
<el-button
|
||||
v-if="contractSnapshot"
|
||||
class="btn-ghost"
|
||||
@click="handleViewContract"
|
||||
>
|
||||
<el-icon><View /></el-icon>查看合同
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="contractSelectorVisible = true">
|
||||
<el-icon><Search /></el-icon>选择合同
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!contractSnapshot" class="pl-plan-form-empty-hint">请选择 CRM 合同,系统将自动带入供方、需方与产品信息</p>
|
||||
<p v-if="!contractSnapshot" class="pl-plan-form-empty-hint">请选择 CRM 合同,系统将自动带入合同与产品信息</p>
|
||||
|
||||
<template v-else>
|
||||
<dl class="pl-plan-kv-grid">
|
||||
<div class="pl-plan-kv"><dt>合同编号</dt><dd>{{ contractSnapshot.contractNo || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>合同名称</dt><dd>{{ contractSnapshot.contractName || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>签订日期</dt><dd>{{ contractSnapshot.signDate || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>合同有效期</dt><dd>{{ contractSnapshot.startDate || '—' }} ~ {{ contractSnapshot.endDate || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>结算方式</dt><dd>
|
||||
<DictTag
|
||||
v-if="contractSnapshot.settlementMethod"
|
||||
dict-type="crm_settlement_method"
|
||||
:value="contractSnapshot.settlementMethod"
|
||||
/>
|
||||
<span v-else>—</span>
|
||||
</dd></div>
|
||||
<div class="pl-plan-kv"><dt>合同金额</dt><dd>{{ formatMoneyDisplay(contractSnapshot.amount) }}</dd></div>
|
||||
</dl>
|
||||
|
||||
<div class="pl-plan-party-row">
|
||||
<div class="pl-plan-party-block">
|
||||
<h4 class="pl-plan-party-title">供方信息</h4>
|
||||
<dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
|
||||
<div class="pl-plan-kv"><dt>名称</dt><dd>{{ contractSnapshot.supplierName || contractSnapshot.partyA || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>联系人</dt><dd>{{ contractSnapshot.partyAContact || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>电话</dt><dd>{{ contractSnapshot.partyAMobile || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv pl-plan-kv-span"><dt>地址</dt><dd>{{ contractSnapshot.partyAAddress || '—' }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-flow-bridge" aria-hidden="true">
|
||||
<el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-party-block">
|
||||
<h4 class="pl-plan-party-title">需方信息</h4>
|
||||
<dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
|
||||
<div class="pl-plan-kv"><dt>名称</dt><dd>{{ contractSnapshot.customerName || contractSnapshot.partyB || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>联系人</dt><dd>{{ contractSnapshot.partyBContact || contractSnapshot.contactName || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv"><dt>电话</dt><dd>{{ contractSnapshot.partyBMobile || contractSnapshot.contactMobile || '—' }}</dd></div>
|
||||
<div class="pl-plan-kv pl-plan-kv-span"><dt>地址</dt><dd>{{ contractSnapshot.partyBAddress || '—' }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pl-plan-contract-kv-rows">
|
||||
<ViewKvTable :fields="contractBasicRow1Fields" mode="triple" />
|
||||
<ViewKvTable :fields="contractBasicRow2Fields" mode="triple" />
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">合同产品预览</h4>
|
||||
@@ -141,20 +113,19 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-if="vehicleBundle" class="pl-plan-vehicle-detail">
|
||||
<PlVehicleViewPanel
|
||||
collapsible
|
||||
:default-expanded="false"
|
||||
:detail="vehicleBundle.detail"
|
||||
:personnel-list="vehicleBundle.personnelList"
|
||||
/>
|
||||
</div>
|
||||
<PlVehicleSummaryStrip
|
||||
v-if="vehicleBundle"
|
||||
:detail="vehicleBundle.detail"
|
||||
:personnel-list="vehicleBundle.personnelList"
|
||||
:carrier-name="summaryCarrierName"
|
||||
@view="vehicleViewVisible = true"
|
||||
/>
|
||||
|
||||
<div class="pl-plan-logistics-row">
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">取货信息</h4>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="取货地址" class="form-col-span-2">
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="取货地址">
|
||||
<el-input v-model="formData.pickupAddress" placeholder="取货地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="取货时间">
|
||||
@@ -173,13 +144,30 @@
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-flow-bridge" aria-hidden="true">
|
||||
<el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
|
||||
<span class="pl-plan-flow-bridge__label">运输</span>
|
||||
<span class="pl-plan-flow-bridge__arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path
|
||||
d="M5 12h11.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.75"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.5 8.5 18.5 12l-5 3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">送货信息</h4>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="送货地址" class="form-col-span-2">
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="送货地址">
|
||||
<el-input v-model="formData.destinationAddress" placeholder="送货地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="送达时间">
|
||||
@@ -271,17 +259,25 @@
|
||||
</template>
|
||||
|
||||
<ContractSelector v-model:visible="contractSelectorVisible" @confirm="handleContractConfirm" />
|
||||
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" />
|
||||
<PlVehicleViewDialog
|
||||
v-model:visible="vehicleViewVisible"
|
||||
:vehicle-id="formData.vehicleId"
|
||||
:bundle="vehicleBundle"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Right, Search } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Search, View } from '@element-plus/icons-vue'
|
||||
import { getContract } from '@/api/crm/contract'
|
||||
import type { ContractVO } from '@/api/crm/contract/types'
|
||||
import { carrierApi, planApi, transportApi, vehicleApi, type Carrier, type Plan, type PlanProduct, type PlanUpdatePayload, type Vehicle, type VehiclePersonnelRow } from '@/api/pl'
|
||||
import ContractSelector from '@/components/business/contract-selector/index.vue'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import PlVehicleViewPanel from '@/components/business/pl-vehicle-view-panel/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
|
||||
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
|
||||
import PlPlanFormProductTable from './PlPlanFormProductTable.vue'
|
||||
import {
|
||||
applyContractToPlanForm,
|
||||
@@ -294,6 +290,10 @@ import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
|
||||
const ContractDetailDialog = defineAsyncComponent(
|
||||
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
|
||||
)
|
||||
|
||||
const SELF_CARRIER_NAME = '安庆市山峡化工有限公司'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -343,6 +343,9 @@ const innerVisible = computed({
|
||||
const submitLoading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const contractSelectorVisible = ref(false)
|
||||
const contractDetailDialogActive = ref(false)
|
||||
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
|
||||
const vehicleViewVisible = ref(false)
|
||||
const vehicleLoading = ref(false)
|
||||
const basicFormRef = ref()
|
||||
const contractSnapshot = ref<ContractVO | null>(null)
|
||||
@@ -413,6 +416,52 @@ const isSelfVehicle = computed(
|
||||
selectedVehicle.value?.vehicleType === 'self' || vehicleBundle.value?.detail?.vehicleType === 'self'
|
||||
)
|
||||
|
||||
const summaryCarrierName = computed(() => {
|
||||
if (isSelfVehicle.value) return SELF_CARRIER_NAME
|
||||
const carrier = carrierList.value.find(item => item.id === formData.carrierId)
|
||||
if (carrier?.carrierName) return carrier.carrierName
|
||||
return vehicleBundle.value?.detail?.carrierName || ''
|
||||
})
|
||||
|
||||
function buildContractBasicFields(contract: ContractVO): ViewField[] {
|
||||
const settlementMethod = contract.settlementMethod
|
||||
return [
|
||||
{ label: '合同编号', value: contract.contractNo, long: false },
|
||||
{ label: '合同名称', value: contract.contractName, long: false },
|
||||
{ label: '签订日期', value: contract.signDate, long: false },
|
||||
{
|
||||
label: '合同有效期',
|
||||
value:
|
||||
contract.startDate && contract.endDate
|
||||
? `${contract.startDate} ~ ${contract.endDate}`
|
||||
: contract.startDate || contract.endDate || '—',
|
||||
long: false
|
||||
},
|
||||
{
|
||||
label: '结算方式',
|
||||
value: settlementMethod ? dictStore.getLabel('crm_settlement_method', settlementMethod) : '—',
|
||||
tagType: settlementMethod ? dictStore.getTagType('crm_settlement_method', settlementMethod) : undefined,
|
||||
long: false
|
||||
},
|
||||
{
|
||||
label: '合同金额(元)',
|
||||
value: formatMoneyDisplay(contract.amount),
|
||||
money: true,
|
||||
long: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const contractBasicFieldRows = computed(() => {
|
||||
const contract = contractSnapshot.value
|
||||
if (!contract) return { row1: [] as ViewField[], row2: [] as ViewField[] }
|
||||
const fields = buildContractBasicFields(contract)
|
||||
return { row1: fields.slice(0, 3), row2: fields.slice(3, 6) }
|
||||
})
|
||||
|
||||
const contractBasicRow1Fields = computed(() => contractBasicFieldRows.value.row1)
|
||||
const contractBasicRow2Fields = computed(() => contractBasicFieldRows.value.row2)
|
||||
|
||||
function resolveSelfCarrierId() {
|
||||
const matched = carrierList.value.find(item => item.carrierName === SELF_CARRIER_NAME)
|
||||
formData.carrierId = matched?.id || ''
|
||||
@@ -456,9 +505,7 @@ function mapPlanProductToFormRow(item: PlanProduct, index: number): PlanFormProd
|
||||
categoryName: item.categoryName,
|
||||
unit: item.unit || '吨',
|
||||
quantity: Number(item.quantity || item.inboundEstimateQty || item.outboundEstimateQty || 0),
|
||||
actualQty: Number(
|
||||
item.actualQty ?? item.quantity ?? item.inboundEstimateQty ?? item.outboundEstimateQty ?? 0
|
||||
),
|
||||
actualQty: 0,
|
||||
packageSpec: item.packageSpec,
|
||||
deliveryMethod: item.deliveryMethod,
|
||||
taxRate: undefined,
|
||||
@@ -484,7 +531,7 @@ function mapProductsToSubmitPayload() {
|
||||
packageSpec: item.packageSpec,
|
||||
deliveryMethod: item.deliveryMethod,
|
||||
quantity: item.quantity,
|
||||
actualQty: item.actualQty,
|
||||
actualQty: 0,
|
||||
taxPrice: item.taxPrice,
|
||||
taxAmount: item.taxAmount,
|
||||
taxSubtotal: item.taxSubtotal,
|
||||
@@ -777,6 +824,18 @@ async function loadMasterData() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewContract() {
|
||||
const contractId = formData.contractId || contractSnapshot.value?.id
|
||||
if (!contractId) {
|
||||
ElMessage.warning('请先选择合同')
|
||||
return
|
||||
}
|
||||
|
||||
contractDetailDialogActive.value = true
|
||||
await nextTick()
|
||||
contractDetailRef.value?.open(contractId)
|
||||
}
|
||||
|
||||
async function handleContractConfirm(contract: ContractVO) {
|
||||
const mapped = applyContractToPlanForm(contract)
|
||||
contractSnapshot.value = contract
|
||||
@@ -908,6 +967,13 @@ async function handleSubmit() {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pl-plan-form-section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pl-plan-form-section-title {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 10px;
|
||||
@@ -936,6 +1002,25 @@ async function handleSubmit() {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.pl-plan-contract-kv-rows {
|
||||
:deep(.view-kv-table-wrap) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(.view-kv-table-wrap:first-child) {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
:deep(.view-kv-table-wrap:last-child) {
|
||||
border-radius: 0 0 8px 8px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:deep(.view-kv-table-wrap:only-child) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@@ -959,7 +1044,7 @@ async function handleSubmit() {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
margin-top: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pl-plan-party-row .pl-plan-party-block,
|
||||
@@ -969,17 +1054,44 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
.pl-plan-flow-bridge {
|
||||
flex: 0 0 72px;
|
||||
flex: 0 0 88px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.pl-plan-flow-bridge__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
letter-spacing: 0.06em;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.pl-plan-flow-bridge__arrow {
|
||||
font-size: 28px;
|
||||
color: #10b981;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
color: var(--primary-dark, #059669);
|
||||
background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
border: 1px solid #a7f3d0;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.14);
|
||||
|
||||
svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-kv-grid-party {
|
||||
@@ -987,10 +1099,22 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
.pl-plan-logistics-block {
|
||||
padding: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
|
||||
.pl-plan-subtitle {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-grid-3 {
|
||||
gap: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-party-block {
|
||||
@@ -1007,14 +1131,6 @@ async function handleSubmit() {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.pl-plan-vehicle-detail {
|
||||
margin: 12px 0;
|
||||
padding: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.pl-plan-kv-grid,
|
||||
.pl-plan-kv-grid-party {
|
||||
@@ -1031,7 +1147,8 @@ async function handleSubmit() {
|
||||
.pl-plan-flow-bridge {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
padding: 12px 0;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.pl-plan-flow-bridge__arrow {
|
||||
|
||||
@@ -5,34 +5,13 @@
|
||||
<el-tab-pane label="计划概况" name="overview">
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">基础信息</h3>
|
||||
<dl class="pl-plan-kv-grid">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>计划编号</dt>
|
||||
<dd>{{ plan?.planNo || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>计划日期</dt>
|
||||
<dd>{{ plan?.planDate || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>归属公司</dt>
|
||||
<dd>{{ getPlPlanCompanyLabel(plan?.belongsCompany) }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>单据类型</dt>
|
||||
<dd>{{ getPlPlanTypeLabel(plan?.planType || '') }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>跟进人</dt>
|
||||
<dd>{{ plan?.followUserName || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<ViewKvTable :fields="overviewBasicViewFields" mode="quad" />
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">产品信息</h3>
|
||||
<p v-if="!productRows.length && !loading" class="pl-plan-form-empty-hint">暂无产品明细</p>
|
||||
<PlPlanFormProductTable v-else :products="productRows" />
|
||||
<PlPlanFormProductTable v-else :products="productRows" show-actual-qty />
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
@@ -81,131 +60,61 @@
|
||||
|
||||
<el-tab-pane label="运输信息" name="transport">
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">运输信息</h3>
|
||||
<dl class="pl-plan-kv-grid">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>车牌号</dt>
|
||||
<dd>{{ plan?.licensePlate || transport?.licensePlate || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>运输公司</dt>
|
||||
<dd>{{ displayCarrierName || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<h3 class="pl-plan-form-section-title">车辆与承运</h3>
|
||||
<PlVehicleSummaryStrip
|
||||
v-if="vehicleBundle"
|
||||
:detail="vehicleBundle.detail"
|
||||
:personnel-list="vehicleBundle.personnelList"
|
||||
:carrier-name="summaryCarrierName"
|
||||
@view="vehicleViewVisible = true"
|
||||
/>
|
||||
<p v-else-if="!loading" class="pl-plan-form-empty-hint">暂无车辆信息</p>
|
||||
|
||||
<div v-if="vehicleBundle" class="pl-plan-vehicle-detail">
|
||||
<PlVehicleViewPanel
|
||||
collapsible
|
||||
:default-expanded="false"
|
||||
:detail="vehicleBundle.detail"
|
||||
:personnel-list="vehicleBundle.personnelList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-logistics-row">
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">取货信息</h4>
|
||||
<dl class="pl-plan-kv-grid pl-plan-kv-grid-2">
|
||||
<div class="pl-plan-kv pl-plan-kv-span">
|
||||
<dt>取货地址</dt>
|
||||
<dd>{{ plan?.pickupAddress || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>取货时间</dt>
|
||||
<dd>{{ plan?.pickupTime || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>注意事项</dt>
|
||||
<dd>{{ plan?.pickupNotes || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<h4 class="pl-plan-subtitle">物流节点</h4>
|
||||
<div class="parties-row">
|
||||
<div class="party-panel">
|
||||
<div class="party-panel__title">取货信息</div>
|
||||
<ViewKvTable :fields="pickupViewFields" mode="quad" />
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-flow-bridge" aria-hidden="true">
|
||||
<el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
|
||||
<div class="party-flow-bridge" aria-hidden="true">
|
||||
<span class="party-flow-bridge__label">运输</span>
|
||||
<span class="party-flow-bridge__arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path
|
||||
d="M5 12h11.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.75"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.5 8.5 18.5 12l-5 3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">送货信息</h4>
|
||||
<dl class="pl-plan-kv-grid pl-plan-kv-grid-2">
|
||||
<div class="pl-plan-kv pl-plan-kv-span">
|
||||
<dt>送货地址</dt>
|
||||
<dd>{{ plan?.destinationAddress || plan?.destinationName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>送达时间</dt>
|
||||
<dd>{{ plan?.deliveryTime || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>注意事项</dt>
|
||||
<dd>{{ plan?.destinationNotes || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="party-panel">
|
||||
<div class="party-panel__title">送货信息</div>
|
||||
<ViewKvTable :fields="deliveryViewFields" mode="quad" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<dl class="pl-plan-kv-grid">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>是否有运输费用</dt>
|
||||
<dd>{{ getPlYesNoLabel(plan?.hasFreight) }}</dd>
|
||||
</div>
|
||||
<template v-if="plan?.hasFreight === 1">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>运输公里数</dt>
|
||||
<dd>{{ displayDistanceKm }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>运费计算方式</dt>
|
||||
<dd>{{ getPlFreightTypeLabel(displayFreightType) }}</dd>
|
||||
</div>
|
||||
<div v-if="displayFreightType === 'by_weight'" class="pl-plan-kv">
|
||||
<dt>运输量(吨)</dt>
|
||||
<dd>{{ displayTransportQty }}</dd>
|
||||
</div>
|
||||
<div v-if="displayFreightType === 'by_weight'" class="pl-plan-kv">
|
||||
<dt>运输单价(元/吨)</dt>
|
||||
<dd class="money-display">{{ displayFreightPrice }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>运费金额(元)</dt>
|
||||
<dd class="money-display">{{ displayFreightAmount }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>是否开票</dt>
|
||||
<dd>{{ getPlYesNoLabel(transport?.hasInvoice) }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
</dl>
|
||||
<ViewKvTable :fields="freightViewFields" mode="quad" />
|
||||
|
||||
<h4 class="pl-plan-subtitle">司机回传信息</h4>
|
||||
<p v-if="!hasDriverFeedback" class="pl-plan-form-empty-hint">暂无司机回传</p>
|
||||
<template v-else>
|
||||
<dl class="pl-plan-kv-grid">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>回传时间</dt>
|
||||
<dd>{{ displayFeedbackTime }}</dd>
|
||||
</div>
|
||||
<div v-if="transport?.feedbackQty != null" class="pl-plan-kv">
|
||||
<dt>回传数量</dt>
|
||||
<dd>{{ transport?.feedbackQty }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv pl-plan-kv-span">
|
||||
<dt>回传备注</dt>
|
||||
<dd class="pl-plan-view-remark">{{ transport?.feedbackRemark || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv pl-plan-kv-span">
|
||||
<dt>回传附件</dt>
|
||||
<dd>
|
||||
<AttachmentUploadField
|
||||
v-if="feedbackImageCsv"
|
||||
:model-value="feedbackImageCsv"
|
||||
disabled
|
||||
/>
|
||||
<span v-else>—</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<ViewKvTable :fields="driverFeedbackViewFields" mode="quad" />
|
||||
<div v-if="feedbackImageCsv" class="pl-plan-feedback-attachments">
|
||||
<span class="pl-plan-feedback-attachments__label">回传附件</span>
|
||||
<AttachmentUploadField :model-value="feedbackImageCsv" disabled />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h4 class="pl-plan-subtitle">地磅单信息</h4>
|
||||
@@ -364,122 +273,85 @@
|
||||
|
||||
<el-tab-pane label="合同信息" name="contract">
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">合同信息</h3>
|
||||
<p v-if="!contractSnapshot" class="pl-plan-form-empty-hint">暂无关联合同信息</p>
|
||||
<template v-else>
|
||||
<dl class="pl-plan-kv-grid">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>合同编号</dt>
|
||||
<dd>{{ contractSnapshot.contractNo || '—' }}</dd>
|
||||
<div class="pl-plan-form-section-head">
|
||||
<h3 class="pl-plan-form-section-title">基本信息</h3>
|
||||
<div class="pl-plan-form-section-actions">
|
||||
<el-button class="btn-ghost" @click="handleViewContract">
|
||||
<el-icon><View /></el-icon>查看合同
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>合同名称</dt>
|
||||
<dd>{{ contractSnapshot.contractName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>签订日期</dt>
|
||||
<dd>{{ contractSnapshot.signDate || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>合同有效期</dt>
|
||||
<dd
|
||||
>{{ contractSnapshot.startDate || '—' }} ~
|
||||
{{ contractSnapshot.endDate || '—' }}</dd
|
||||
>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>结算方式</dt>
|
||||
<dd>
|
||||
<DictTag
|
||||
v-if="contractSnapshot.settlementMethod"
|
||||
dict-type="crm_settlement_method"
|
||||
:value="contractSnapshot.settlementMethod"
|
||||
/>
|
||||
<span v-else>—</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>合同金额</dt>
|
||||
<dd class="money-display">{{ formatMoneyDisplay(contractSnapshot.amount) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<ViewKvTable :fields="contractBasicViewFields" mode="triple" />
|
||||
|
||||
<div class="pl-plan-party-row">
|
||||
<div class="pl-plan-party-block">
|
||||
<h4 class="pl-plan-party-title">供方信息</h4>
|
||||
<dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>名称</dt>
|
||||
<dd>{{ contractSnapshot.supplierName || contractSnapshot.partyA || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>联系人</dt>
|
||||
<dd>{{ contractSnapshot.partyAContact || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>电话</dt>
|
||||
<dd>{{ contractSnapshot.partyAMobile || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv pl-plan-kv-span">
|
||||
<dt>地址</dt>
|
||||
<dd>{{ contractSnapshot.partyAAddress || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<h4 class="pl-plan-subtitle">供需双方</h4>
|
||||
<div class="parties-row">
|
||||
<div class="party-panel">
|
||||
<div class="party-panel__title">供方信息</div>
|
||||
<ViewKvTable :fields="contractSupplierViewFields" mode="dual" />
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-flow-bridge" aria-hidden="true">
|
||||
<el-icon class="pl-plan-flow-bridge__arrow"><Right /></el-icon>
|
||||
<div class="party-flow-bridge" aria-hidden="true">
|
||||
<span class="party-flow-bridge__label">供货</span>
|
||||
<span class="party-flow-bridge__arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path
|
||||
d="M5 12h11.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.75"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M13.5 8.5 18.5 12l-5 3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-party-block">
|
||||
<h4 class="pl-plan-party-title">需方信息</h4>
|
||||
<dl class="pl-plan-kv-grid pl-plan-kv-grid-party">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>名称</dt>
|
||||
<dd>{{ contractSnapshot.customerName || contractSnapshot.partyB || '—' }}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>联系人</dt>
|
||||
<dd>{{
|
||||
contractSnapshot.partyBContact || contractSnapshot.contactName || '—'
|
||||
}}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>电话</dt>
|
||||
<dd>{{
|
||||
contractSnapshot.partyBMobile || contractSnapshot.contactMobile || '—'
|
||||
}}</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv pl-plan-kv-span">
|
||||
<dt>地址</dt>
|
||||
<dd>{{ contractSnapshot.partyBAddress || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="party-panel">
|
||||
<div class="party-panel__title">需方信息</div>
|
||||
<ViewKvTable :fields="contractBuyerViewFields" mode="dual" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">合同产品预览</h4>
|
||||
<PlPlanFormProductTable :products="productRows" />
|
||||
<PlPlanFormProductTable :products="contractProductRows" />
|
||||
</template>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" />
|
||||
<PlVehicleViewDialog
|
||||
v-model:visible="vehicleViewVisible"
|
||||
:vehicle-id="plan?.vehicleId"
|
||||
:bundle="vehicleBundle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Right } from '@element-plus/icons-vue'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
|
||||
import PlVehicleViewPanel from '@/components/business/pl-vehicle-view-panel/index.vue'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
|
||||
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import {
|
||||
businessTypeTagType,
|
||||
inventoryStatusTagType
|
||||
} from '@/components/business/db/db-related-view'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import type { ContractVO } from '@/api/crm/contract/types'
|
||||
import { mapContractProducts, type PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { mapPlanProductToFormRow, usePlPlanViewDetail } from '@/composables/usePlPlanViewDetail'
|
||||
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
getPlFreightTypeLabel,
|
||||
@@ -491,8 +363,14 @@
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import type { Transport } from '@/api/pl'
|
||||
|
||||
const ContractDetailDialog = defineAsyncComponent(
|
||||
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
|
||||
)
|
||||
|
||||
const SELF_CARRIER_NAME = '安庆市山峡化工有限公司'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
planId: string
|
||||
@@ -502,6 +380,9 @@
|
||||
)
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const contractDetailDialogActive = ref(false)
|
||||
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
|
||||
const vehicleViewVisible = ref(false)
|
||||
const {
|
||||
loading,
|
||||
plan,
|
||||
@@ -565,12 +446,151 @@
|
||||
return amount ? formatMoneyDisplay(Number(amount)) : '—'
|
||||
})
|
||||
|
||||
const productRows = computed<PlanFormProductRow[]>(() =>
|
||||
products.value.map((item, index) => mapPlanProductToFormRow(item, index))
|
||||
function mergeTaxRatesFromContract(
|
||||
rows: PlanFormProductRow[],
|
||||
contract: ContractVO | null
|
||||
): PlanFormProductRow[] {
|
||||
if (!contract?.products?.length) return rows
|
||||
const rateByProductId = new Map<string, number>()
|
||||
for (const item of contract.products) {
|
||||
const key = String(item.productId ?? item.id ?? '')
|
||||
if (key && item.taxRate != null) rateByProductId.set(key, Number(item.taxRate))
|
||||
}
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
taxRate: row.taxRate ?? rateByProductId.get(row.productId)
|
||||
}))
|
||||
}
|
||||
|
||||
const overviewBasicViewFields = computed<ViewField[]>(() => [
|
||||
{ label: '计划编号', value: plan.value?.planNo },
|
||||
{ label: '计划日期', value: plan.value?.planDate },
|
||||
{ label: '归属公司', value: getPlPlanCompanyLabel(plan.value?.belongsCompany) },
|
||||
{ label: '单据类型', value: getPlPlanTypeLabel(plan.value?.planType || '') },
|
||||
{ label: '跟进人', value: plan.value?.followUserName }
|
||||
])
|
||||
|
||||
const summaryCarrierName = computed(() => displayCarrierName.value)
|
||||
|
||||
const pickupViewFields = computed<ViewField[]>(() => [
|
||||
{ label: '取货地址', value: plan.value?.pickupAddress, span: 'full' },
|
||||
{ label: '取货时间', value: plan.value?.pickupTime },
|
||||
{ label: '注意事项', value: plan.value?.pickupNotes, span: 'full' }
|
||||
])
|
||||
|
||||
const deliveryViewFields = computed<ViewField[]>(() => [
|
||||
{
|
||||
label: '送货地址',
|
||||
value: plan.value?.destinationAddress || plan.value?.destinationName,
|
||||
span: 'full'
|
||||
},
|
||||
{ label: '送达时间', value: plan.value?.deliveryTime },
|
||||
{ label: '注意事项', value: plan.value?.destinationNotes, span: 'full' }
|
||||
])
|
||||
|
||||
const freightViewFields = computed<ViewField[]>(() => {
|
||||
const fields: ViewField[] = [
|
||||
{ label: '是否有运输费用', value: getPlYesNoLabel(plan.value?.hasFreight) }
|
||||
]
|
||||
if (plan.value?.hasFreight !== 1) return fields
|
||||
|
||||
fields.push(
|
||||
{ label: '运输公里数', value: displayDistanceKm.value },
|
||||
{ label: '运费计算方式', value: getPlFreightTypeLabel(displayFreightType.value) }
|
||||
)
|
||||
if (displayFreightType.value === 'by_weight') {
|
||||
fields.push(
|
||||
{ label: '运输量(吨)', value: displayTransportQty.value },
|
||||
{ label: '运输单价(元/吨)', value: displayFreightPrice.value, money: true }
|
||||
)
|
||||
}
|
||||
fields.push(
|
||||
{ label: '运费金额(元)', value: displayFreightAmount.value, money: true },
|
||||
{ label: '是否开票', value: getPlYesNoLabel(transport.value?.hasInvoice) }
|
||||
)
|
||||
return fields
|
||||
})
|
||||
|
||||
const driverFeedbackViewFields = computed<ViewField[]>(() => {
|
||||
const fields: ViewField[] = [{ label: '回传时间', value: displayFeedbackTime.value }]
|
||||
if (transport.value?.feedbackQty != null) {
|
||||
fields.push({ label: '回传数量', value: transport.value.feedbackQty })
|
||||
}
|
||||
fields.push({ label: '回传备注', value: transport.value?.feedbackRemark, span: 'full' })
|
||||
return fields
|
||||
})
|
||||
|
||||
const contractBasicViewFields = computed<ViewField[]>(() => {
|
||||
const c = contractSnapshot.value
|
||||
if (!c) return []
|
||||
const settlementLabel = c.settlementMethod
|
||||
? dictStore.getLabel('crm_settlement_method', c.settlementMethod)
|
||||
: '—'
|
||||
return [
|
||||
{ label: '合同编号', value: c.contractNo, long: false },
|
||||
{ label: '合同名称', value: c.contractName, long: false },
|
||||
{ label: '签订日期', value: c.signDate },
|
||||
{
|
||||
label: '合同有效期',
|
||||
value: c.startDate && c.endDate ? `${c.startDate} ~ ${c.endDate}` : '—',
|
||||
long: false
|
||||
},
|
||||
{ label: '结算方式', value: settlementLabel },
|
||||
{ label: '合同金额(元)', value: formatMoneyDisplay(c.amount), money: true }
|
||||
]
|
||||
})
|
||||
|
||||
const contractSupplierViewFields = computed<ViewField[]>(() => {
|
||||
const c = contractSnapshot.value
|
||||
if (!c) return []
|
||||
return [
|
||||
{ label: '单位名称(章)', value: c.partyA },
|
||||
{ label: '单位地址', value: c.partyAAddress },
|
||||
{ label: '法定代表人', value: c.partyALegalPerson },
|
||||
{ label: '代理人', value: c.partyAAgent },
|
||||
{ label: '电话/传真', value: c.partyAPhoneFax },
|
||||
{ label: '开户银行', value: c.partyABank },
|
||||
{ label: '账号', value: c.partyAAccount }
|
||||
]
|
||||
})
|
||||
|
||||
const contractBuyerViewFields = computed<ViewField[]>(() => {
|
||||
const c = contractSnapshot.value
|
||||
if (!c) return []
|
||||
return [
|
||||
{ label: '单位名称(章)', value: c.partyB },
|
||||
{ label: '单位地址', value: c.partyBAddress },
|
||||
{ label: '法定代表人', value: c.partyBLegalPerson },
|
||||
{ label: '代理人', value: c.partyBAgent },
|
||||
{ label: '电话/传真', value: c.partyBPhoneFax },
|
||||
{ label: '开户银行', value: c.partyBBank },
|
||||
{ label: '账号', value: c.partyBAccount }
|
||||
]
|
||||
})
|
||||
|
||||
const contractProductRows = computed<PlanFormProductRow[]>(() =>
|
||||
mapContractProducts(contractSnapshot.value?.products)
|
||||
)
|
||||
|
||||
const productRows = computed<PlanFormProductRow[]>(() => {
|
||||
const rows = products.value.map((item, index) => mapPlanProductToFormRow(item, index))
|
||||
return mergeTaxRatesFromContract(rows, contractSnapshot.value)
|
||||
})
|
||||
|
||||
const hazardProducts = computed(() => products.value.filter((item) => isHazardProduct(item)))
|
||||
|
||||
async function handleViewContract() {
|
||||
const contractId = contractSnapshot.value?.id || plan.value?.contractId
|
||||
if (!contractId) {
|
||||
ElMessage.warning('暂无关联合同')
|
||||
return
|
||||
}
|
||||
|
||||
contractDetailDialogActive.value = true
|
||||
await nextTick()
|
||||
contractDetailRef.value?.open(contractId)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.planId,
|
||||
async (planId) => {
|
||||
@@ -622,6 +642,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-form-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pl-plan-form-section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pl-plan-form-section-title {
|
||||
margin: 0 0 12px;
|
||||
padding-left: 10px;
|
||||
@@ -632,6 +667,10 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pl-plan-form-section-head .pl-plan-form-section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pl-plan-subtitle {
|
||||
margin: 16px 0 10px;
|
||||
font-size: 13px;
|
||||
@@ -687,54 +726,106 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.pl-plan-party-row,
|
||||
.pl-plan-logistics-row {
|
||||
.parties-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
margin-top: 12px;
|
||||
margin-top: 4px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pl-plan-party-row .pl-plan-party-block,
|
||||
.pl-plan-logistics-row .pl-plan-logistics-block {
|
||||
.party-panel {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border, #d9e8e2);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pl-plan-flow-bridge {
|
||||
flex: 0 0 72px;
|
||||
.party-panel__title {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
line-height: 1.5;
|
||||
border-left: 3px solid var(--primary, #10b981);
|
||||
}
|
||||
|
||||
.party-flow-bridge {
|
||||
flex: 0 0 88px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.pl-plan-flow-bridge__arrow {
|
||||
font-size: 28px;
|
||||
color: #10b981;
|
||||
.party-flow-bridge__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
letter-spacing: 0.06em;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.pl-plan-kv-grid-party {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
.party-flow-bridge__arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
color: var(--primary-dark, #059669);
|
||||
background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
border: 1px solid #a7f3d0;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.14);
|
||||
|
||||
svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-logistics-block,
|
||||
.pl-plan-party-block {
|
||||
@media (max-width: 1100px) {
|
||||
.parties-row {
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.party-flow-bridge {
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.party-flow-bridge__arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-feedback-attachments {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pl-plan-party-title {
|
||||
margin: 0 0 10px;
|
||||
.pl-plan-feedback-attachments__label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.pl-plan-vehicle-detail {
|
||||
margin-top: 12px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-plan-cert-block {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="pl-vehicle-summary-strip">
|
||||
<div class="pl-vehicle-summary-strip__content">
|
||||
<div class="pl-vehicle-summary-group">
|
||||
<span class="pl-vehicle-summary-label">车辆</span>
|
||||
<span class="pl-vehicle-summary-value pl-vehicle-summary-value--strong">
|
||||
{{ detail.licensePlate || '—' }}
|
||||
</span>
|
||||
<el-tag :type="getPlVehicleTypeTagType(detail.vehicleType)" size="small">
|
||||
{{ getPlVehicleTypeLabel(detail.vehicleType) }}
|
||||
</el-tag>
|
||||
<span class="pl-vehicle-summary-value">{{ formatLoadCapacity(detail.loadCapacity) }}(载重)</span>
|
||||
<span v-if="displayCarrierName" class="pl-vehicle-summary-value">
|
||||
{{ displayCarrierName }}(承运商)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="pl-vehicle-summary-divider" aria-hidden="true" />
|
||||
|
||||
<div class="pl-vehicle-summary-group">
|
||||
<span class="pl-vehicle-summary-label">人员</span>
|
||||
<span v-if="!personnelList.length" class="pl-vehicle-summary-value muted">暂无关联人员</span>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(person, index) in personnelList"
|
||||
:key="person.personnelId || `${person.personnelName}-${index}`"
|
||||
class="pl-vehicle-summary-person"
|
||||
>
|
||||
<el-tag :type="getRoleTagType(person.role)" size="small">{{ getRoleLabel(person.role) }}</el-tag>
|
||||
<span class="pl-vehicle-summary-value">{{ person.personnelName || '—' }}</span>
|
||||
<span v-if="person.phoneNo" class="pl-vehicle-summary-value muted">{{ person.phoneNo }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button link type="primary" class="pl-vehicle-summary-view-btn" @click="emit('view')">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import type { VehicleDetail } from '@/api/pl'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import { getPlVehicleTypeLabel, getPlVehicleTypeTagType } from '@/utils/pl-display'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
detail: VehicleDetail
|
||||
personnelList?: VehiclePersonnelViewRow[]
|
||||
/** 展示用承运商名称,优先于 detail.carrierName */
|
||||
carrierName?: string
|
||||
}>(),
|
||||
{
|
||||
personnelList: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const displayCarrierName = computed(
|
||||
() => props.carrierName || props.detail.carrierName || ''
|
||||
)
|
||||
|
||||
function formatLoadCapacity(value?: number | string | null) {
|
||||
if (value == null || value === '') return '— 吨'
|
||||
const num = Number(value)
|
||||
if (Number.isNaN(num)) return `${value} 吨`
|
||||
return `${num.toFixed(2)} 吨`
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
view: []
|
||||
}>()
|
||||
|
||||
function getRoleLabel(role: string) {
|
||||
const map: Record<string, string> = {
|
||||
driver: '司机',
|
||||
escort: '押运员'
|
||||
}
|
||||
return map[role] || role
|
||||
}
|
||||
|
||||
function getRoleTagType(role: string) {
|
||||
return role === 'driver' ? 'primary' : 'warning'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-vehicle-summary-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pl-vehicle-summary-strip__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 14px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-vehicle-summary-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-vehicle-summary-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-vehicle-summary-value {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
|
||||
&.muted {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&--strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-vehicle-summary-person {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pl-vehicle-summary-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: #cbd5e1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pl-vehicle-summary-view-btn {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<SystemViewDialog
|
||||
v-model:visible="innerVisible"
|
||||
:title="detail?.licensePlate || '车辆详情'"
|
||||
:icon="Van"
|
||||
:meta="heroMeta"
|
||||
:sections="viewSections"
|
||||
:loading="loading"
|
||||
width="1040px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
>
|
||||
<template #vehicleDetailTabs>
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:cert-list="detail?.certList || []"
|
||||
:personnel-list="personnelList"
|
||||
/>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Van } from '@element-plus/icons-vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import SystemViewDialog, {
|
||||
type ViewSection
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
loadVehicleViewBundle,
|
||||
type VehicleViewBundle
|
||||
} from '@/composables/usePlVehicleViewDetail'
|
||||
import type { VehicleDetail } from '@/api/pl'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
getPlVehicleStatusLabel,
|
||||
getPlVehicleStatusTagType,
|
||||
getPlVehicleTypeLabel,
|
||||
getPlVehicleTypeTagType
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
vehicleId?: string
|
||||
bundle?: VehicleViewBundle | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: value => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref<VehicleDetail | null>(null)
|
||||
const personnelList = ref<VehiclePersonnelViewRow[]>([])
|
||||
|
||||
const heroMeta = computed(() => {
|
||||
if (!detail.value) return ''
|
||||
const parts = [
|
||||
getPlVehicleTypeLabel(detail.value.vehicleType),
|
||||
getPlVehicleStatusLabel(detail.value.vehicleStatus),
|
||||
detail.value.carrierName || undefined
|
||||
].filter(Boolean)
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = detail.value
|
||||
if (!d) return []
|
||||
|
||||
return [
|
||||
{
|
||||
title: '车辆信息',
|
||||
fieldLayout: 'table',
|
||||
tableMode: 'quad',
|
||||
fields: [
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{
|
||||
label: '车辆类型',
|
||||
value: getPlVehicleTypeLabel(d.vehicleType),
|
||||
tagType: getPlVehicleTypeTagType(d.vehicleType)
|
||||
},
|
||||
{ label: '承运商', value: d.carrierName || '—' },
|
||||
{ label: '载重(吨)', value: d.loadCapacity ?? '—' },
|
||||
{
|
||||
label: '车辆状态',
|
||||
value: getPlVehicleStatusLabel(d.vehicleStatus),
|
||||
tagType: getPlVehicleStatusTagType(d.vehicleStatus),
|
||||
long: false
|
||||
},
|
||||
{ label: '创建时间', value: formatDateTime(d.createTime), long: false },
|
||||
{ label: '关联人员摘要', value: d.personnelSummary || '—', long: false },
|
||||
{ label: '备注', value: d.remark || '—', long: false }
|
||||
],
|
||||
slot: 'vehicleDetailTabs'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.vehicleId, props.bundle] as const,
|
||||
async ([visible, vehicleId, bundle]) => {
|
||||
if (!visible) {
|
||||
detail.value = null
|
||||
personnelList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (bundle) {
|
||||
detail.value = bundle.detail
|
||||
personnelList.value = bundle.personnelList
|
||||
return
|
||||
}
|
||||
|
||||
if (!vehicleId) return
|
||||
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
personnelList.value = []
|
||||
try {
|
||||
const loaded = await loadVehicleViewBundle(vehicleId)
|
||||
detail.value = loaded.detail
|
||||
personnelList.value = loaded.personnelList
|
||||
} catch {
|
||||
innerVisible.value = false
|
||||
ElMessage.error('加载车辆详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
Reference in new issue
Block a user