3362 lines
111 KiB
Vue
3362 lines
111 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="formTitle"
|
||
width="100%"
|
||
top="20px"
|
||
class="dialog-form-layout contract-form-dialog"
|
||
destroy-on-close
|
||
append-to-body
|
||
modal-class="order-module-dialog-mask"
|
||
:close-on-click-modal="false"
|
||
@opened="handleOpened"
|
||
>
|
||
<div
|
||
class="contract-form-body"
|
||
v-loading="formLoading"
|
||
element-loading-text="正在加载合同数据..."
|
||
>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
label-position="right"
|
||
label-width="120px"
|
||
class="crm-dialog-form"
|
||
:disabled="formLoading"
|
||
>
|
||
<!-- 合同基础信息(一行 4 列) -->
|
||
<div class="contract-basic-info">
|
||
<el-form-item label="合同编号" prop="contractNo">
|
||
<el-input v-model="form.contractNo" placeholder="系统自动生成,格式:SX/X-JT20250926002" maxlength="50" />
|
||
</el-form-item>
|
||
<el-form-item label="合同名称" prop="contractName">
|
||
<el-input
|
||
v-model="form.contractName"
|
||
placeholder="可不填,自动生成:供方简称-需方简称(合同编号)"
|
||
maxlength="100"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同类型" prop="contractType" required>
|
||
<el-select v-model="form.contractType" placeholder="请选择合同类型" style="width: 100%" @change="handleContractTypeChange">
|
||
<el-option v-for="dict in dictStore.getOptions('crm_contract_type')" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="合同模版" prop="contractTemplate" required>
|
||
<el-select v-model="form.contractTemplate" placeholder="请选择合同模版" style="width: 100%" @change="handleContractTemplateChange">
|
||
<el-option v-for="dict in dictStore.getOptions('crm_contract_template')" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="客户名称" prop="customerId" required>
|
||
<div class="customer-select-row">
|
||
<el-select
|
||
v-model="form.customerId"
|
||
class="customer-select-row__select"
|
||
filterable
|
||
remote
|
||
clearable
|
||
:remote-method="searchCustomerOptions"
|
||
:loading="customerOptionsLoading"
|
||
placeholder="请输入客户名称/拼音首字母搜索"
|
||
@focus="handleCustomerSelectFocus"
|
||
@change="handleCustomerSelectChange"
|
||
>
|
||
<el-option
|
||
v-for="item in customerOptions"
|
||
:key="item.id"
|
||
:label="item.customerName"
|
||
:value="item.id"
|
||
>
|
||
<div class="customer-option-row">
|
||
<span>{{ item.customerName }}</span>
|
||
<span v-if="item.customerNo" class="customer-option-row__meta">{{ item.customerNo }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
<el-button text class="customer-select-row__search" @click="handleSelectCustomer">
|
||
<el-icon><Search /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="关联报价单" prop="quoteId">
|
||
<el-input v-model="form.quoteNo" placeholder="请选择报价单(可选)" readonly clearable>
|
||
<template #append>
|
||
<el-button text @click="handleSelectQuote">
|
||
<el-icon><Search /></el-icon>
|
||
</el-button>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="联系人" prop="contactId" required>
|
||
<div class="customer-select-row">
|
||
<el-select
|
||
v-model="form.contactId"
|
||
class="customer-select-row__select"
|
||
filterable
|
||
clearable
|
||
:disabled="!form.customerId"
|
||
:loading="contactOptionsLoading"
|
||
:placeholder="form.customerId ? '请选择联系人' : '请先选择客户'"
|
||
@visible-change="handleContactSelectVisible"
|
||
@change="handleContactSelectChange"
|
||
>
|
||
<el-option
|
||
v-for="item in contactOptions"
|
||
:key="item.id"
|
||
:label="item.contactName"
|
||
:value="item.id"
|
||
>
|
||
<div class="customer-option-row">
|
||
<span>
|
||
{{ item.contactName }}
|
||
<el-tag v-if="item.isPrimary === 1" type="success" size="small" style="margin-left: 6px">主要</el-tag>
|
||
</span>
|
||
<span v-if="item.mobile" class="customer-option-row__meta">{{ item.mobile }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
<el-button
|
||
text
|
||
class="customer-select-row__search"
|
||
:disabled="!form.customerId"
|
||
@click="handleSelectContact"
|
||
>
|
||
<el-icon><Search /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="联系电话" prop="contactsPhone">
|
||
<el-input v-model="form.contactsPhone" placeholder="从客户/联系人带出" maxlength="20" />
|
||
</el-form-item>
|
||
<el-form-item label="签订日期" prop="signDate">
|
||
<el-date-picker
|
||
v-model="form.signDate"
|
||
type="date"
|
||
placeholder="选择签订日期"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
@change="handleSignDateChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="负责人" prop="ownerUserId" required>
|
||
<el-select
|
||
v-model="form.ownerUserId"
|
||
placeholder="请选择负责人"
|
||
filterable
|
||
clearable
|
||
style="width: 100%"
|
||
@change="handleOwnerChange"
|
||
>
|
||
<el-option
|
||
v-for="user in ownerUserList"
|
||
:key="user.userId"
|
||
:label="user.nickName"
|
||
:value="user.userId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="合同金额(元)" prop="amount">
|
||
<el-input-number
|
||
v-model="form.amount"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="可选填,留空则从产品明细自动计算"
|
||
style="width: 100%"
|
||
class="money-field-input"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="运费(元)" prop="freight">
|
||
<el-input-number
|
||
v-model="form.freight"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="可选,选择报价单后可自动带入"
|
||
style="width: 100%"
|
||
class="money-field-input"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="桶押金(元)" prop="barrelDeposit">
|
||
<el-input-number
|
||
v-model="form.barrelDeposit"
|
||
:precision="2"
|
||
:min="0"
|
||
placeholder="可选,选择报价单后可自动带入"
|
||
style="width: 100%"
|
||
class="money-field-input"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同期限" prop="startDate" required>
|
||
<el-date-picker
|
||
v-model="contractDateRange"
|
||
type="daterange"
|
||
range-separator="-"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="YYYY-MM-DD"
|
||
style="width: 100%"
|
||
@change="handleDateRangeChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="交易主体" prop="supplierId" required>
|
||
<el-select v-model="form.supplierId" placeholder="请选择交易主体" clearable style="width: 100%" @change="handleSupplierChange">
|
||
<el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="结算方式" prop="settlementMethod" required>
|
||
<el-select v-model="form.settlementMethod" placeholder="请选择结算方式" clearable style="width: 100%">
|
||
<el-option v-for="dict in dictStore.getOptions('crm_settlement_method')" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<!-- 供方 / 需方信息(左右布局,默认缩略、各自独立展开) -->
|
||
<div class="parties-section">
|
||
<div class="parties-section-toolbar">
|
||
<el-button text type="primary" size="small" @click="partiesExpanded = !partiesExpanded">
|
||
<el-icon>
|
||
<ArrowUp v-if="partiesExpanded" />
|
||
<ArrowDown v-else />
|
||
</el-icon>
|
||
{{ partiesExpanded ? '收起供需方' : '展开供需方' }}
|
||
</el-button>
|
||
<el-button type="primary" size="small" @click="handleSwapParties">
|
||
<el-icon><Sort /></el-icon>
|
||
供需方调换
|
||
</el-button>
|
||
</div>
|
||
<div class="parties-row">
|
||
<div class="party-panel" :class="{ 'is-collapsed': !partiesExpanded }">
|
||
<div class="party-panel__header">
|
||
<div class="party-panel__title">供方信息</div>
|
||
</div>
|
||
<div v-if="!partiesExpanded" class="party-panel__summary">
|
||
<span class="party-panel__summary-label">单位名称</span>
|
||
<span class="party-panel__summary-value" :class="{ 'is-empty': !form.partyA }">
|
||
{{ form.partyA || '未填写' }}
|
||
</span>
|
||
</div>
|
||
<div v-show="partiesExpanded" class="form-grid-2">
|
||
<el-form-item label="单位名称(章)" prop="partyA">
|
||
<el-input v-model="form.partyA" placeholder="请输入单位名称" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="单位地址" prop="partyAAddress">
|
||
<el-input v-model="form.partyAAddress" placeholder="请输入单位地址" maxlength="200" />
|
||
</el-form-item>
|
||
<el-form-item label="法定代表人" prop="partyALegalPerson">
|
||
<el-input v-model="form.partyALegalPerson" placeholder="请输入法定代表人" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="代理人" prop="partyAAgent">
|
||
<el-input v-model="form.partyAAgent" placeholder="请输入代理人(可选)" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="电话/传真" prop="partyAPhoneFax">
|
||
<el-input v-model="form.partyAPhoneFax" placeholder="请输入电话/传真" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="开户银行" prop="partyABank">
|
||
<el-input v-model="form.partyABank" placeholder="请输入开户银行" maxlength="200" />
|
||
</el-form-item>
|
||
<el-form-item label="账号" prop="partyAAccount">
|
||
<el-input v-model="form.partyAAccount" placeholder="请输入账号" maxlength="100" />
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<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="party-panel" :class="{ 'is-collapsed': !partiesExpanded }">
|
||
<div class="party-panel__header">
|
||
<div class="party-panel__title">需方信息</div>
|
||
</div>
|
||
<div v-if="!partiesExpanded" class="party-panel__summary">
|
||
<span class="party-panel__summary-label">单位名称</span>
|
||
<span class="party-panel__summary-value" :class="{ 'is-empty': !form.partyB }">
|
||
{{ form.partyB || '未填写' }}
|
||
</span>
|
||
</div>
|
||
<div v-show="partiesExpanded" class="form-grid-2">
|
||
<el-form-item label="单位名称(章)" prop="partyB">
|
||
<el-input v-model="form.partyB" placeholder="从客户开票信息带出" maxlength="200" />
|
||
</el-form-item>
|
||
<el-form-item label="单位地址" prop="partyBAddress">
|
||
<el-input v-model="form.partyBAddress" placeholder="从客户开票信息带出" maxlength="500" />
|
||
</el-form-item>
|
||
<el-form-item label="法定代表人" prop="partyBLegalPerson">
|
||
<el-input v-model="form.partyBLegalPerson" placeholder="从客户开票信息带出" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="代理人" prop="partyBAgent">
|
||
<el-input v-model="form.partyBAgent" placeholder="从客户开票信息带出(可选)" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="电话/传真" prop="partyBPhoneFax">
|
||
<el-input v-model="form.partyBPhoneFax" placeholder="从客户开票信息带出" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="开户银行" prop="partyBBank">
|
||
<el-input v-model="form.partyBBank" placeholder="从客户开票信息带出" maxlength="200" />
|
||
</el-form-item>
|
||
<el-form-item label="账号" prop="partyBAccount">
|
||
<el-input v-model="form.partyBAccount" placeholder="从客户开票信息带出" maxlength="100" />
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 产品明细部分 -->
|
||
<el-divider content-position="left">产品明细</el-divider>
|
||
<div class="panel dialog-list-panel mb-4">
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
<el-button class="btn-main" type="primary" @click="handleAddProduct">
|
||
<el-icon><Plus /></el-icon>添加产品
|
||
</el-button>
|
||
<el-button class="btn-ghost" @click="handleRemoveSelectedProducts" :disabled="selectedProducts.length === 0">
|
||
<el-icon><Delete /></el-icon>删除选中
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="list-body list-wrap">
|
||
<ListTable table-key="crm-contract-components-contract-form-dialog" :data="form.products" class="list-table" stripe @selection-change="handleProductSelectionChange">
|
||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||
<el-table-column label="序号" type="index" width="60" align="center" header-align="center" />
|
||
<el-table-column label="产品名称" min-width="160" :show-overflow-tooltip="true">
|
||
<template #default="scope">
|
||
<el-input
|
||
:model-value="scope.row.standardProductName || scope.row.productName"
|
||
placeholder="产品名称"
|
||
clearable
|
||
size="small"
|
||
readonly
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="别名" min-width="140">
|
||
<template #default="scope">
|
||
<div class="line-meta-cell">
|
||
<el-select
|
||
v-if="(scope.row.aliasOptions || []).length"
|
||
v-model="scope.row.aliasName"
|
||
placeholder="不选则用产品名称"
|
||
clearable
|
||
filterable
|
||
size="small"
|
||
class="line-meta-cell__control"
|
||
>
|
||
<el-option
|
||
v-for="name in scope.row.aliasOptions"
|
||
:key="name"
|
||
:label="name"
|
||
:value="name"
|
||
/>
|
||
</el-select>
|
||
<span v-else class="text-muted line-meta-cell__empty">—</span>
|
||
<el-button
|
||
v-if="scope.row.productId"
|
||
link
|
||
type="primary"
|
||
class="line-meta-cell__btn"
|
||
@click="openAliasBrandManage(scope.row, 'alias')"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="品牌" min-width="140">
|
||
<template #default="scope">
|
||
<div class="line-meta-cell">
|
||
<el-select
|
||
v-if="(scope.row.brandOptions || []).length"
|
||
v-model="scope.row.brandId"
|
||
placeholder="不限品牌"
|
||
clearable
|
||
filterable
|
||
size="small"
|
||
class="line-meta-cell__control"
|
||
@change="(val) => handleBrandIdChange(scope.row, val)"
|
||
>
|
||
<el-option
|
||
v-for="b in scope.row.brandOptions"
|
||
:key="b.id"
|
||
:label="b.brandName"
|
||
:value="b.id"
|
||
/>
|
||
</el-select>
|
||
<span v-else class="text-muted line-meta-cell__empty">—</span>
|
||
<el-button
|
||
v-if="scope.row.productId"
|
||
link
|
||
type="primary"
|
||
class="line-meta-cell__btn"
|
||
@click="openAliasBrandManage(scope.row, 'brand')"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="型号/规格" prop="productModel" min-width="100" :show-overflow-tooltip="true">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.productModel" placeholder="请输入型号/规格" clearable size="small" readonly />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="税率(%)" prop="taxRate" min-width="80">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.taxRate" placeholder="税率" @input="handleTaxRateChange(scope.row)" clearable size="small" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="单位" prop="unit" width="100" align="center">
|
||
<template #default="scope">
|
||
<DictSelect
|
||
v-model="scope.row.unit"
|
||
dict-type="crm_product_unit"
|
||
placeholder="请选择单位"
|
||
size="small"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="quantity" width="130">
|
||
<template #header> <span style="color: #f56c6c">*</span> 数量</template>
|
||
<template #default="scope">
|
||
<div v-if="isFrameworkContract" class="framework-pricing-cell">
|
||
<template v-if="isFrameworkQuantityCustomerDemand(scope.row)">
|
||
<span class="text-muted framework-pricing-cell__label">{{ FRAMEWORK_QUANTITY_LABEL }}</span>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
class="framework-pricing-cell__action"
|
||
@click="handleEnableFrameworkQuantityFixed(scope.row)"
|
||
>
|
||
填数字
|
||
</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-input
|
||
v-model="scope.row.quantity"
|
||
placeholder="数量"
|
||
@input="handleQuantityChange(scope.row)"
|
||
clearable
|
||
size="small"
|
||
/>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
class="framework-pricing-cell__action"
|
||
@click="handleResetFrameworkQuantity(scope.row)"
|
||
>
|
||
按客户需求
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
<el-input
|
||
v-else
|
||
v-model="scope.row.quantity"
|
||
placeholder="数量"
|
||
@input="handleQuantityChange(scope.row)"
|
||
clearable
|
||
size="small"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="price" width="150">
|
||
<template #header>
|
||
<span v-if="isFrameworkContract">含税单价(元)</span>
|
||
<span v-else><span style="color: #f56c6c">*</span> 含税单价(元)</span>
|
||
</template>
|
||
<template #default="scope">
|
||
<div v-if="isFrameworkContract" class="framework-pricing-cell">
|
||
<template v-if="isFrameworkPriceMarket(scope.row)">
|
||
<span class="text-muted framework-pricing-cell__label">{{ FRAMEWORK_PRICE_LABEL }}</span>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
class="framework-pricing-cell__action"
|
||
@click="handleEnableFrameworkPriceFixed(scope.row)"
|
||
>
|
||
填数字
|
||
</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-input
|
||
v-model="scope.row.price"
|
||
placeholder="含税单价"
|
||
@input="handlePriceChange(scope.row)"
|
||
clearable
|
||
size="small"
|
||
class="money-field-input"
|
||
/>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
class="framework-pricing-cell__action"
|
||
@click="handleResetFrameworkPrice(scope.row)"
|
||
>
|
||
随行就市
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
<el-input
|
||
v-else
|
||
v-model="scope.row.price"
|
||
placeholder="含税单价"
|
||
@input="handlePriceChange(scope.row)"
|
||
clearable
|
||
size="small"
|
||
class="money-field-input"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="subtotal" width="150" align="right">
|
||
<template #header>
|
||
<span v-if="isFrameworkContract">含税总价(元)</span>
|
||
<span v-else><span style="color: #f56c6c">*</span> 含税总价(元)</span>
|
||
</template>
|
||
<template #default="scope">
|
||
<div v-if="isFrameworkContract" class="framework-pricing-cell framework-pricing-cell--money">
|
||
<template v-if="isFrameworkPriceMarket(scope.row)">
|
||
<span class="text-muted framework-pricing-cell__label">{{ FRAMEWORK_PRICE_LABEL }}</span>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
class="framework-pricing-cell__action"
|
||
@click="handleEnableFrameworkPriceFixed(scope.row)"
|
||
>
|
||
填数字
|
||
</el-button>
|
||
</template>
|
||
<span v-else class="money-display">{{ formatFrameworkMoneyDisplay(scope.row, 'subtotal') }}</span>
|
||
</div>
|
||
<span v-else class="money-display">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="税费(元)" prop="taxAmount" min-width="120" align="right">
|
||
<template #default="scope">
|
||
<div v-if="isFrameworkContract" class="framework-pricing-cell framework-pricing-cell--money">
|
||
<template v-if="isFrameworkPriceMarket(scope.row)">
|
||
<span class="text-muted framework-pricing-cell__label">{{ FRAMEWORK_PRICE_LABEL }}</span>
|
||
<el-button
|
||
link
|
||
type="primary"
|
||
class="framework-pricing-cell__action"
|
||
@click="handleEnableFrameworkPriceFixed(scope.row)"
|
||
>
|
||
填数字
|
||
</el-button>
|
||
</template>
|
||
<span v-else class="money-display">{{ formatFrameworkMoneyDisplay(scope.row, 'taxAmount') }}</span>
|
||
</div>
|
||
<span v-else class="money-display">¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="packageSpec" min-width="150">
|
||
<template #header> <span style="color: #f56c6c">*</span> 包装规格</template>
|
||
<template #default="scope">
|
||
<DictSelect
|
||
v-model="scope.row.packageSpec"
|
||
dict-type="crm_product_gg"
|
||
placeholder="请选择包装规格"
|
||
size="small"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="deliveryMethod" min-width="150">
|
||
<template #header> <span style="color: #f56c6c">*</span> 送货方式</template>
|
||
<template #default="scope">
|
||
<DictSelect
|
||
v-model="scope.row.deliveryMethod"
|
||
dict-type="crm_shfs"
|
||
placeholder="请选择送货方式"
|
||
size="small"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="160">
|
||
<template #default="scope">
|
||
<el-input
|
||
v-model="scope.row.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="请输入备注"
|
||
maxlength="500"
|
||
show-word-limit
|
||
clearable
|
||
size="small"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" min-width="80" align="center" header-align="center" fixed="right" class-name="col-actions">
|
||
<template #default="scope">
|
||
<el-button link type="danger" @click="handleRemoveProduct(scope.$index)">
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</ListTable>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 框架合同调价区间 -->
|
||
<template v-if="isFrameworkContractType(form.contractType)">
|
||
<el-divider content-position="left">框架合同调价区间</el-divider>
|
||
<p class="text-muted mb-3" style="font-size: 13px">
|
||
按产品维护生效日期区间与含税单价(可选);若填写区间则须完整(日期、单价、调价函)。合同总金额不与此联动,仍为 0。
|
||
</p>
|
||
<div v-for="(prod, pIdx) in form.products" :key="'fw-tier-' + pIdx" class="framework-tier-block mb-4">
|
||
<div class="fw-tier-title mb-2">{{ prod.productName || '产品' + (pIdx + 1) }}</div>
|
||
<el-button type="primary" link class="mb-2 fw-tier-letter-add-btn" @click="addPriceTierRow(pIdx)">
|
||
<el-icon><Plus /></el-icon>调价函
|
||
</el-button>
|
||
<div class="panel dialog-list-panel mb-3">
|
||
<div class="list-body list-wrap dialog-list-body-only">
|
||
<ListTable table-key="crm-contract-components-contract-form-dialog-2" :data="prod.priceTiers || []" class="list-table fw-price-tier-table" stripe empty-text="暂无调价区间">
|
||
<el-table-column type="index" label="#" width="50" align="center" header-align="center" />
|
||
<el-table-column label="开始日期" min-width="150">
|
||
<template #default="scope">
|
||
<el-date-picker v-model="scope.row.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width: 100%" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结束日期" min-width="150">
|
||
<template #default="scope">
|
||
<el-date-picker v-model="scope.row.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width: 100%" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="含税单价(元)" width="140">
|
||
<template #default="scope">
|
||
<el-input v-model="scope.row.price" placeholder="含税单价" clearable size="small" class="money-field-input" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="调价函" min-width="220">
|
||
<template #default="scope">
|
||
<AttachmentUploadField
|
||
:model-value="tierLetterCsv(scope.row)"
|
||
accept="image/*"
|
||
:limit="20"
|
||
compact
|
||
:biz-type="CRM_UPLOAD_BIZ_TYPE.CONTRACT_PRICE_LETTER"
|
||
@update:model-value="syncTierLetterCsv(scope.row, $event)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" min-width="70" align="center" header-align="center" fixed="right" class-name="col-actions">
|
||
<template #default="scope">
|
||
<el-button link type="danger" @click="removePriceTierRow(pIdx, scope.$index)">
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</ListTable>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<el-divider />
|
||
<div class="total-section">
|
||
<el-row :gutter="20">
|
||
<el-col :span="16"></el-col>
|
||
<el-col :span="8">
|
||
<div class="total-item">
|
||
<span class="total-label">合计金额(元):</span>
|
||
<span class="total-value money-display">¥{{ parseFloat(form.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- 附件上传部分 - OpenSpec 标准卡片化布局 -->
|
||
<el-divider content-position="left">附件上传</el-divider>
|
||
<el-form-item label="附件" prop="attachmentFiles" :required="form.contractTemplate === 'customer_template'">
|
||
<ContractAttachmentPanel
|
||
v-model="attachmentList"
|
||
:biz-type="CRM_UPLOAD_BIZ_TYPE.CONTRACT_ATTACHMENT"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button class="btn-ghost" @click="handleClose">
|
||
<el-icon><Close /></el-icon>取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-ghost"
|
||
:loading="saveDraftLoading"
|
||
:disabled="formLoading"
|
||
@click="handleSaveDraft"
|
||
>
|
||
<el-icon><FolderChecked /></el-icon>保存草稿
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
:loading="submitLoading"
|
||
:disabled="formLoading"
|
||
@click="handleSubmit"
|
||
>
|
||
<el-icon><Promotion /></el-icon>提交审批
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 产品选择器 -->
|
||
<ProductSelector
|
||
:visible="productSelectorVisible"
|
||
:supplier-id="form.supplierId"
|
||
@update:visible="productSelectorVisible = $event"
|
||
@confirm="handleProductSelected"
|
||
/>
|
||
|
||
<ProductAliasBrandDialog
|
||
v-model="aliasBrandManageVisible"
|
||
:product-id="aliasBrandManageProductId"
|
||
:product-name="aliasBrandManageProductName"
|
||
:mode="aliasBrandManageMode"
|
||
@saved="handleAliasBrandManaged"
|
||
/>
|
||
|
||
<!-- 客户选择器 -->
|
||
<CustomerSelector
|
||
:visible="customerSelectorVisible"
|
||
@update:visible="customerSelectorVisible = $event"
|
||
@confirm="handleCustomerSelectorConfirm"
|
||
/>
|
||
|
||
<!-- 下拉选客户时补填简称 -->
|
||
<el-dialog
|
||
v-model="shortNameDialogVisible"
|
||
title="填写客户简称"
|
||
width="480px"
|
||
align-center
|
||
class="dialog-form-layout"
|
||
modal-class="order-module-dialog-mask"
|
||
:close-on-click-modal="false"
|
||
append-to-body
|
||
@closed="handleShortNameDialogClosed"
|
||
>
|
||
<el-form
|
||
ref="shortNameFormRef"
|
||
:model="shortNameForm"
|
||
:rules="shortNameRules"
|
||
label-position="top"
|
||
class="crm-dialog-form"
|
||
>
|
||
<el-form-item label="客户名称">
|
||
<el-input :model-value="shortNameForm.customerName" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="客户简称" prop="shortName">
|
||
<el-input
|
||
v-model="shortNameForm.shortName"
|
||
placeholder="请输入客户简称"
|
||
maxlength="100"
|
||
show-word-limit
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button class="btn-ghost" @click="shortNameDialogVisible = false">
|
||
<el-icon><Close /></el-icon>取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" :loading="shortNameSaving" @click="handleSaveShortName">
|
||
<el-icon><Check /></el-icon>确定
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 联系人选择器 -->
|
||
<ContactSelector
|
||
:visible="contactSelectorVisible"
|
||
@update:visible="contactSelectorVisible = $event"
|
||
:customerId="form.customerId"
|
||
@confirm="handleContactConfirm"
|
||
/>
|
||
|
||
<!-- 下拉无联系人时补充 -->
|
||
<ContactFormDialog ref="contactFormRef" @success="handleContactFormSuccess" />
|
||
|
||
<!-- 报价单选择器 -->
|
||
<QuoteSelector
|
||
:visible="quoteSelectorVisible"
|
||
@update:visible="quoteSelectorVisible = $event"
|
||
:customerId="form.customerId"
|
||
@confirm="handleQuoteConfirm"
|
||
/>
|
||
|
||
<!-- 用户选择器 -->
|
||
|
||
<!-- 审批弹窗 -->
|
||
<ApprovalDialog ref="approvalDialogRef" @success="handleApprovalSuccess" />
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, nextTick, watch } from 'vue'
|
||
import type { FormInstance, FormItemRule } from 'element-plus'
|
||
import { ElMessage } from 'element-plus'
|
||
import {
|
||
getContract,
|
||
addContract,
|
||
updateContract,
|
||
type ContractForm,
|
||
type ContractProductForm,
|
||
type ContractProductPriceTierForm
|
||
} from '@/api/crm/contract/index'
|
||
import { isFrameworkContractType, isPurchaseLikeContractType } from '../contractType'
|
||
import {
|
||
FRAMEWORK_QUANTITY_LABEL,
|
||
FRAMEWORK_PRICE_LABEL,
|
||
applyFrameworkDefaultsToProduct,
|
||
createFrameworkProductPricingDefaults,
|
||
enableFrameworkPriceFixed,
|
||
enableFrameworkQuantityFixed,
|
||
formatFrameworkMoneyDisplay,
|
||
isBlankNumeric,
|
||
isFrameworkPriceMarket,
|
||
isFrameworkQuantityCustomerDemand,
|
||
isNegativeNumeric,
|
||
confirmZeroPricingIfNeeded,
|
||
normalizeFrameworkProductPricing,
|
||
prepareFrameworkProductForSubmit,
|
||
resetFrameworkPriceToMarket,
|
||
resetFrameworkQuantityToCustomerDemand
|
||
} from '../frameworkProductPricing'
|
||
import { useDictStore } from '@/store/modules/dict'
|
||
import { useUserStore } from '@/store/modules/user'
|
||
import { $modal } from '@/utils/modal'
|
||
import {
|
||
Document,
|
||
Delete,
|
||
Sort,
|
||
Plus,
|
||
Search,
|
||
Close,
|
||
Check,
|
||
FolderChecked,
|
||
Promotion,
|
||
Loading,
|
||
ArrowDown,
|
||
ArrowUp
|
||
} from '@element-plus/icons-vue'
|
||
import {
|
||
buildUploadFileListFromField,
|
||
serializeContractAttachments,
|
||
type ContractAttachmentItem
|
||
} from '@/utils/crm-attachment'
|
||
import { CRM_UPLOAD_BIZ_TYPE } from '@/utils/crm-upload'
|
||
import ContractAttachmentPanel from './ContractAttachmentPanel.vue'
|
||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||
import { getOpportunity } from '@/api/crm/opportunity'
|
||
import { getQuote } from '@/api/crm/quote'
|
||
import ProductSelector from '@/components/business/product-selector/index.vue'
|
||
import ProductAliasBrandDialog from '@/components/business/product-alias-brand-dialog/index.vue'
|
||
import DictSelect from '@/components/DictSelect/index.vue'
|
||
import CustomerSelector from '@/components/CustomerSelector.vue'
|
||
import ContactSelector from '@/components/ContactSelector.vue'
|
||
import ContactFormDialog from '@/views/crm/customer/components/ContactFormDialog.vue'
|
||
import QuoteSelector from '@/components/QuoteSelector.vue'
|
||
import { fetchContactsByCustomerId } from '@/api/crm/contact'
|
||
import { removeProductsNotAllowedByHazard } from '@/utils/crm-supplier-hazard'
|
||
import {
|
||
applyBrandSelectChange,
|
||
applyManagedProductMetaToLines,
|
||
initLineBrandMetaFromProduct,
|
||
prepareLineAliasAndBrandForSubmit,
|
||
resolveAliasFromSavedName
|
||
} from '@/utils/crm-product-brand'
|
||
import { fetchProductDetail } from '@/api/crm/product'
|
||
import ApprovalDialog from './ApprovalDialog.vue'
|
||
import { omitEmptyExceptPartyFields } from '../partyFields'
|
||
import {
|
||
fetchCustomerList,
|
||
fetchCustomerDetail,
|
||
fetchUpdateCustomer
|
||
} from '@/api/crm/customer'
|
||
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types'
|
||
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo'
|
||
import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo'
|
||
import { fetchUserOptions } from '@/api/admin'
|
||
import { filterOwnerCandidates, findDefaultOwnerUser } from '@/utils/crm-owner-candidates'
|
||
|
||
interface UserVO {
|
||
userId: string | number
|
||
nickName: string
|
||
userName?: string
|
||
}
|
||
|
||
// 扩展表单类型,添加显示用字段
|
||
type ExtendedContractForm = ContractForm & {
|
||
customerName?: string
|
||
opportunityName?: string
|
||
quoteNo?: string
|
||
contactsName?: string
|
||
contactsPhone?: string
|
||
ownerUserName?: string
|
||
supplierId?: number
|
||
settlementMethod?: string
|
||
templateContent?: string
|
||
}
|
||
|
||
// 扩展产品类型,添加显示字段
|
||
type PriceTierRow = ContractProductPriceTierForm
|
||
|
||
type ExtendedContractProductForm = ContractProductForm & {
|
||
productName?: string
|
||
productModel?: string
|
||
unit?: string
|
||
unitPrice?: number
|
||
subtotal?: number
|
||
priceTiers?: PriceTierRow[]
|
||
}
|
||
|
||
const props = defineProps<{
|
||
visible: boolean
|
||
editId: string | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:visible': [value: boolean]
|
||
success: []
|
||
}>()
|
||
|
||
const dialogVisible = computed({
|
||
get: () => props.visible,
|
||
set: (val) => emit('update:visible', val)
|
||
})
|
||
|
||
const dictStore = useDictStore()
|
||
const userStore = useUserStore()
|
||
|
||
// 表单标题
|
||
const formTitle = computed(() => {
|
||
return form.value.id ? '编辑合同' : '新增合同'
|
||
})
|
||
|
||
// 合同期限日期范围
|
||
const contractDateRange = ref<[string, string]>([])
|
||
|
||
// 表单数据
|
||
const form = ref<ExtendedContractForm>({
|
||
id: undefined,
|
||
contractNo: undefined,
|
||
contractName: '',
|
||
contractType: 'sale',
|
||
contractTemplate: 'shanxia',
|
||
customerId: undefined,
|
||
customerName: '',
|
||
contactId: undefined,
|
||
opportunityId: undefined,
|
||
opportunityName: '',
|
||
quoteId: undefined,
|
||
quoteNo: '',
|
||
contactsName: '',
|
||
contactsPhone: '',
|
||
signDate: '',
|
||
startDate: '',
|
||
endDate: '',
|
||
ownerUserId: undefined,
|
||
ownerUserName: '',
|
||
amount: 0,
|
||
freight: 0,
|
||
barrelDeposit: 0,
|
||
supplierId: undefined,
|
||
settlementMethod: undefined,
|
||
partyA: '',
|
||
partyAAddress: '',
|
||
partyAContact: '',
|
||
partyAMobile: '',
|
||
partyALegalPerson: '',
|
||
partyAAgent: '',
|
||
partyAPhoneFax: '',
|
||
partyABank: '',
|
||
partyAAccount: '',
|
||
partyB: '',
|
||
partyBAddress: '',
|
||
partyBContact: '',
|
||
partyBMobile: '',
|
||
partyBLegalPerson: '',
|
||
partyBAgent: '',
|
||
partyBPhoneFax: '',
|
||
partyBBank: '',
|
||
partyBAccount: '',
|
||
paymentTerms: '',
|
||
deliveryTerms: '',
|
||
contractTerms: '',
|
||
products: [] as ExtendedContractProductForm[],
|
||
remark: ''
|
||
})
|
||
|
||
// 供需方是否已调换的标志
|
||
const isPartiesSwapped = ref(false)
|
||
|
||
/** 供方 / 需方信息展开状态(默认缩略,两侧共用) */
|
||
const partiesExpanded = ref(false)
|
||
|
||
const resetPartyExpand = () => {
|
||
partiesExpanded.value = false
|
||
}
|
||
|
||
/** 校验失败时,自动展开供需方面板 */
|
||
const expandPartiesForInvalidFields = (invalidFields?: Record<string, unknown>) => {
|
||
if (!invalidFields) return
|
||
const keys = Object.keys(invalidFields)
|
||
if (keys.some((k) => k.startsWith('partyA') || k.startsWith('partyB'))) {
|
||
partiesExpanded.value = true
|
||
}
|
||
}
|
||
|
||
// 记录原始的模板内容(用于检测是否被意外删除)
|
||
const originalTemplateContent = ref<string>('')
|
||
|
||
// 供方信息列表
|
||
const supplierList = ref<SupplierInfoVO[]>([])
|
||
|
||
// 客户简称 / 开票公司名(用于按供需方面板匹配简称)
|
||
const customerShortName = ref('')
|
||
const customerInvoiceName = ref('')
|
||
|
||
// 表单校验规则
|
||
const rules = ref({
|
||
contractType: [{ required: true, message: '合同类型不能为空', trigger: 'change' }],
|
||
customerId: [{ required: true, message: '客户不能为空', trigger: 'change' }],
|
||
contactId: [{ required: true, message: '联系人不能为空', trigger: 'change' }],
|
||
signDate: [{ required: true, message: '签订日期不能为空', trigger: 'change' }],
|
||
ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'change' }],
|
||
supplierId: [{ required: true, message: '交易主体不能为空', trigger: 'change' }],
|
||
settlementMethod: [{ required: true, message: '结算方式不能为空', trigger: 'change' }],
|
||
startDate: [
|
||
{
|
||
validator: (_rule: any, _value: any, callback: any) => {
|
||
if (!contractDateRange.value || contractDateRange.value.length !== 2 || !contractDateRange.value[0] || !contractDateRange.value[1]) {
|
||
callback(new Error('合同期限不能为空'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: ['change', 'blur']
|
||
}
|
||
],
|
||
attachmentFiles: []
|
||
})
|
||
|
||
// 附件列表
|
||
const attachmentList = ref<ContractAttachmentItem[]>([])
|
||
|
||
const isFrameworkContract = computed(() => isFrameworkContractType(form.value.contractType))
|
||
|
||
// 加载状态
|
||
const formLoading = ref(false)
|
||
const saveDraftLoading = ref(false)
|
||
const submitLoading = ref(false)
|
||
|
||
/** 编辑打开时立即进入加载态,避免空白表单可被误改 */
|
||
watch(
|
||
() => props.visible,
|
||
(visible) => {
|
||
if (visible && props.editId) {
|
||
formLoading.value = true
|
||
} else if (!visible) {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
)
|
||
|
||
// 组件引用
|
||
const formRef = ref<FormInstance>()
|
||
const approvalDialogRef = ref()
|
||
|
||
// 选择器状态
|
||
const customerSelectorVisible = ref(false)
|
||
const contactSelectorVisible = ref(false)
|
||
const opportunitySelectorVisible = ref(false)
|
||
const quoteSelectorVisible = ref(false)
|
||
const userSelectorVisible = ref(false)
|
||
const productSelectorVisible = ref(false)
|
||
|
||
// 客户下拉(远程模糊检索)
|
||
const customerOptions = ref<CustomerVO[]>([])
|
||
const customerOptionsLoading = ref(false)
|
||
/** 已确认选定的客户(用于简称弹窗取消时回滚下拉值) */
|
||
const confirmedCustomerId = ref<string | number | undefined>(undefined)
|
||
const shortNameDialogVisible = ref(false)
|
||
const shortNameSaving = ref(false)
|
||
const shortNameFormRef = ref<FormInstance>()
|
||
const shortNameForm = ref({
|
||
id: '' as string | number,
|
||
customerName: '',
|
||
shortName: ''
|
||
})
|
||
const shortNameRules: Record<string, FormItemRule[]> = {
|
||
shortName: [{ required: true, message: '客户简称不能为空', trigger: 'blur' }]
|
||
}
|
||
const pendingCustomerForShortName = ref<CustomerVO | null>(null)
|
||
const shortNameSavedSuccessfully = ref(false)
|
||
|
||
// 联系人下拉
|
||
const contactOptions = ref<ContactVO[]>([])
|
||
const contactOptionsLoading = ref(false)
|
||
const contactFormRef = ref<InstanceType<typeof ContactFormDialog>>()
|
||
/** 避免 visible-change 重复弹出补充联系人弹窗 */
|
||
const contactEmptyPrompted = ref(false)
|
||
|
||
const hasCustomerShortName = (row?: { shortName?: string } | null) =>
|
||
Boolean(String(row?.shortName || '').trim())
|
||
|
||
const ensureCustomerOption = (customer?: Partial<CustomerVO> | null) => {
|
||
if (!customer?.id) return
|
||
const exists = customerOptions.value.some((item) => String(item.id) === String(customer.id))
|
||
if (exists) {
|
||
customerOptions.value = customerOptions.value.map((item) =>
|
||
String(item.id) === String(customer.id)
|
||
? {
|
||
...item,
|
||
customerName: customer.customerName || item.customerName,
|
||
customerNo: customer.customerNo ?? item.customerNo,
|
||
shortName: customer.shortName ?? item.shortName
|
||
}
|
||
: item
|
||
)
|
||
return
|
||
}
|
||
customerOptions.value = [
|
||
{
|
||
id: customer.id,
|
||
customerName: customer.customerName || '',
|
||
customerNo: customer.customerNo,
|
||
shortName: customer.shortName
|
||
} as CustomerVO,
|
||
...customerOptions.value
|
||
]
|
||
}
|
||
|
||
const extractCustomerList = (res: unknown): CustomerVO[] => {
|
||
const data = (res as { data?: unknown })?.data ?? res ?? {}
|
||
const list = (data as { list?: CustomerVO[]; rows?: CustomerVO[] }).list
|
||
|| (data as { rows?: CustomerVO[] }).rows
|
||
|| []
|
||
return Array.isArray(list) ? list : []
|
||
}
|
||
|
||
const loadDefaultCustomerOptions = async () => {
|
||
customerOptionsLoading.value = true
|
||
try {
|
||
const res = await fetchCustomerList({ page: 1, pageSize: 20 })
|
||
customerOptions.value = extractCustomerList(res)
|
||
if (form.value.customerId) {
|
||
ensureCustomerOption({
|
||
id: form.value.customerId,
|
||
customerName: form.value.customerName,
|
||
shortName: customerShortName.value
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('加载客户列表失败:', error)
|
||
customerOptions.value = []
|
||
} finally {
|
||
customerOptionsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const searchCustomerOptions = async (query: string) => {
|
||
const keyword = query?.trim()
|
||
if (!keyword) {
|
||
await loadDefaultCustomerOptions()
|
||
return
|
||
}
|
||
customerOptionsLoading.value = true
|
||
try {
|
||
const res = await fetchCustomerList({
|
||
customerName: keyword,
|
||
page: 1,
|
||
pageSize: 50
|
||
})
|
||
customerOptions.value = extractCustomerList(res)
|
||
if (form.value.customerId) {
|
||
ensureCustomerOption({
|
||
id: form.value.customerId,
|
||
customerName: form.value.customerName,
|
||
shortName: customerShortName.value
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('搜索客户失败:', error)
|
||
customerOptions.value = []
|
||
} finally {
|
||
customerOptionsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleCustomerSelectFocus = () => {
|
||
if (customerOptions.value.length === 0) {
|
||
loadDefaultCustomerOptions()
|
||
}
|
||
}
|
||
|
||
const extractContactList = (res: unknown): ContactVO[] => {
|
||
const unwrapped = (res as { data?: unknown })?.data ?? res ?? []
|
||
if (Array.isArray(unwrapped)) return unwrapped as ContactVO[]
|
||
const payload = (unwrapped as { list?: ContactVO[]; rows?: ContactVO[] }) || {}
|
||
return payload.list || payload.rows || []
|
||
}
|
||
|
||
const ensureContactOption = (contact?: Partial<ContactVO> | null) => {
|
||
if (!contact?.id) return
|
||
const exists = contactOptions.value.some((item) => String(item.id) === String(contact.id))
|
||
if (exists) {
|
||
contactOptions.value = contactOptions.value.map((item) =>
|
||
String(item.id) === String(contact.id)
|
||
? {
|
||
...item,
|
||
contactName: contact.contactName || item.contactName,
|
||
mobile: contact.mobile ?? item.mobile,
|
||
isPrimary: contact.isPrimary ?? item.isPrimary
|
||
}
|
||
: item
|
||
)
|
||
return
|
||
}
|
||
contactOptions.value = [
|
||
{
|
||
id: contact.id,
|
||
contactName: contact.contactName || '',
|
||
mobile: contact.mobile,
|
||
isPrimary: contact.isPrimary
|
||
} as ContactVO,
|
||
...contactOptions.value
|
||
]
|
||
}
|
||
|
||
const loadContactOptions = async (customerId?: string | number) => {
|
||
if (!customerId) {
|
||
contactOptions.value = []
|
||
return []
|
||
}
|
||
contactOptionsLoading.value = true
|
||
try {
|
||
const res = await fetchContactsByCustomerId({ customerId })
|
||
contactOptions.value = extractContactList(res)
|
||
if (form.value.contactId) {
|
||
ensureContactOption({
|
||
id: form.value.contactId,
|
||
contactName: form.value.contactsName
|
||
})
|
||
}
|
||
return contactOptions.value
|
||
} catch (error) {
|
||
console.error('加载联系人列表失败:', error)
|
||
contactOptions.value = []
|
||
return []
|
||
} finally {
|
||
contactOptionsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const openContactFormDialog = () => {
|
||
if (!form.value.customerId) {
|
||
$modal.msgWarning('请先选择客户')
|
||
return
|
||
}
|
||
contactFormRef.value?.open(undefined, form.value.customerId)
|
||
}
|
||
|
||
const openShortNameDialog = (customer: CustomerVO) => {
|
||
pendingCustomerForShortName.value = customer
|
||
shortNameSavedSuccessfully.value = false
|
||
shortNameForm.value = {
|
||
id: customer.id,
|
||
customerName: customer.customerName || '',
|
||
shortName: customer.shortName || ''
|
||
}
|
||
shortNameDialogVisible.value = true
|
||
nextTick(() => shortNameFormRef.value?.clearValidate())
|
||
}
|
||
|
||
const clearCustomerSelection = () => {
|
||
form.value.customerId = undefined
|
||
form.value.customerName = ''
|
||
customerShortName.value = ''
|
||
customerInvoiceName.value = ''
|
||
confirmedCustomerId.value = undefined
|
||
form.value.contactId = undefined
|
||
form.value.contactsName = ''
|
||
form.value.contactsPhone = ''
|
||
contactOptions.value = []
|
||
contactEmptyPrompted.value = false
|
||
form.value.opportunityId = undefined
|
||
form.value.opportunityName = ''
|
||
form.value.quoteId = undefined
|
||
form.value.quoteNo = ''
|
||
form.value.freight = 0
|
||
form.value.barrelDeposit = 0
|
||
rebuildContractName()
|
||
}
|
||
|
||
// 用户列表(负责人下拉仅方金强 / 吴九华)
|
||
const userList = ref<any[]>([])
|
||
const ownerUserList = computed(() => filterOwnerCandidates(userList.value))
|
||
|
||
// 选中的产品列表
|
||
const selectedProducts = ref<ExtendedContractProductForm[]>([])
|
||
|
||
// 临时接口定义
|
||
interface OpportunityVO {
|
||
id: number
|
||
opportunityName: string
|
||
customerId?: number
|
||
customerName?: string
|
||
}
|
||
|
||
interface QuoteVO {
|
||
id: number
|
||
quoteNo: string
|
||
quoteTitle: string
|
||
customerId?: number
|
||
customerName?: string
|
||
contactId?: number
|
||
contactName?: string
|
||
productList?: any[]
|
||
quoteDate?: string
|
||
}
|
||
|
||
/** 对话框打开后的初始化 */
|
||
const handleOpened = async () => {
|
||
resetPartyExpand()
|
||
const isEdit = !!props.editId
|
||
if (isEdit) {
|
||
formLoading.value = true
|
||
}
|
||
try {
|
||
await Promise.all([loadSupplierList(), loadUserList()])
|
||
|
||
if (props.editId) {
|
||
await loadContractDetail(props.editId)
|
||
} else {
|
||
resetForm()
|
||
form.value.signDate = new Date().toISOString().split('T')[0]
|
||
handleSignDateChange(form.value.signDate)
|
||
applyDefaultOwnerUser()
|
||
|
||
// 设置默认供方主体为"SX"
|
||
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
|
||
if (defaultSupplier) {
|
||
form.value.supplierId = defaultSupplier.id
|
||
await handleSupplierChange(defaultSupplier.id)
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('合同表单初始化失败:', error)
|
||
$modal.msgError(error?.message || '加载合同数据失败')
|
||
if (isEdit) {
|
||
dialogVisible.value = false
|
||
}
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 重置表单 */
|
||
const resetForm = () => {
|
||
form.value = {
|
||
id: undefined,
|
||
contractNo: undefined,
|
||
contractName: '',
|
||
contractType: 'sale',
|
||
contractTemplate: 'shanxia',
|
||
customerId: undefined,
|
||
customerName: '',
|
||
contactId: undefined,
|
||
opportunityId: undefined,
|
||
opportunityName: '',
|
||
quoteId: undefined,
|
||
quoteNo: '',
|
||
contactsName: '',
|
||
contactsPhone: '',
|
||
signDate: '',
|
||
startDate: '',
|
||
endDate: '',
|
||
ownerUserId: undefined,
|
||
ownerUserName: '',
|
||
amount: 0,
|
||
freight: 0,
|
||
barrelDeposit: 0,
|
||
supplierId: undefined,
|
||
settlementMethod: undefined,
|
||
partyA: '',
|
||
partyAAddress: '',
|
||
partyAContact: '',
|
||
partyAMobile: '',
|
||
partyALegalPerson: '',
|
||
partyAAgent: '',
|
||
partyAPhoneFax: '',
|
||
partyABank: '',
|
||
partyAAccount: '',
|
||
partyB: '',
|
||
partyBAddress: '',
|
||
partyBContact: '',
|
||
partyBMobile: '',
|
||
partyBLegalPerson: '',
|
||
partyBAgent: '',
|
||
partyBPhoneFax: '',
|
||
partyBBank: '',
|
||
partyBAccount: '',
|
||
paymentTerms: '',
|
||
deliveryTerms: '',
|
||
contractTerms: '',
|
||
products: [] as ExtendedContractProductForm[],
|
||
remark: ''
|
||
}
|
||
isPartiesSwapped.value = false
|
||
resetPartyExpand()
|
||
originalTemplateContent.value = ''
|
||
customerShortName.value = ''
|
||
customerInvoiceName.value = ''
|
||
confirmedCustomerId.value = undefined
|
||
customerOptions.value = []
|
||
contactOptions.value = []
|
||
contactEmptyPrompted.value = false
|
||
pendingCustomerForShortName.value = null
|
||
shortNameDialogVisible.value = false
|
||
shortNameSavedSuccessfully.value = false
|
||
contractDateRange.value = [] as any
|
||
attachmentList.value = []
|
||
selectedProducts.value = []
|
||
}
|
||
|
||
/** 加载合同详情 */
|
||
const loadContractDetail = async (id: number | string) => {
|
||
const res = await getContract(id)
|
||
const contractData = res.data
|
||
|
||
Object.assign(form.value, contractData)
|
||
|
||
originalTemplateContent.value = contractData.templateContent || ''
|
||
|
||
if (contractData.contactName) {
|
||
form.value.contactsName = contractData.contactName
|
||
}
|
||
if (contractData.contactMobile) {
|
||
form.value.contactsPhone = contractData.contactMobile
|
||
}
|
||
|
||
if (contractData.ownerUserName) {
|
||
form.value.ownerUserName = contractData.ownerUserName
|
||
}
|
||
if (contractData.ownerUserId) {
|
||
form.value.ownerUserId = contractData.ownerUserId
|
||
}
|
||
|
||
if (contractData.opportunityId) {
|
||
try {
|
||
const oppRes = await getOpportunity(contractData.opportunityId)
|
||
if (oppRes.data) {
|
||
form.value.opportunityName = oppRes.data.opportunityName
|
||
}
|
||
} catch (error) {
|
||
console.warn('加载商机信息失败:', error)
|
||
}
|
||
}
|
||
|
||
if (contractData.quoteId) {
|
||
try {
|
||
const quoteRes = await getQuote(contractData.quoteId)
|
||
if (quoteRes.data) {
|
||
form.value.quoteNo = quoteRes.data.quoteNo
|
||
}
|
||
} catch (error) {
|
||
console.warn('加载报价单信息失败:', error)
|
||
}
|
||
}
|
||
|
||
if (contractData.products && Array.isArray(contractData.products)) {
|
||
form.value.products = contractData.products.map((product: any) => ({
|
||
...product,
|
||
productModel: product.model || '',
|
||
isToxic: Number(product.isToxic || 0),
|
||
isExplosive: Number(product.isExplosive || 0),
|
||
brandMode: product.brandMode || undefined,
|
||
brandId: product.brandId || undefined,
|
||
brandName: product.brandName || undefined,
|
||
brandOptions: [],
|
||
priceTiers: (product.priceTiers || []).map((t: any) => {
|
||
let ids: string[] = []
|
||
if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
|
||
ids = t.letterOssIds.map((id: any) => String(id))
|
||
} else if (t.letterOssId != null && t.letterOssId !== '') {
|
||
ids = [String(t.letterOssId)]
|
||
}
|
||
return {
|
||
...t,
|
||
letterOssIds: ids
|
||
}
|
||
}),
|
||
taxRate: product.taxRate || 6,
|
||
taxAmount: product.taxAmount ?? 0,
|
||
packageSpec: product.packageSpec || '',
|
||
deliveryMethod: product.deliveryMethod || ''
|
||
}))
|
||
|
||
await enrichContractProductBrandMeta(form.value.products)
|
||
|
||
const isFw = isFrameworkContractType(contractData.contractType)
|
||
form.value.products.forEach((product: any) => {
|
||
normalizeFrameworkProductPricing(product, isFw)
|
||
if (!isFw || !isFrameworkPriceMarket(product)) {
|
||
calculateProductAmount(product)
|
||
}
|
||
})
|
||
if (isFw) {
|
||
form.value.amount = 0
|
||
} else {
|
||
calculateTotalAmount()
|
||
}
|
||
}
|
||
|
||
if (form.value.startDate && form.value.endDate) {
|
||
contractDateRange.value = [form.value.startDate, form.value.endDate]
|
||
}
|
||
|
||
if (form.value.attachmentFiles) {
|
||
await handleAttachmentData()
|
||
}
|
||
|
||
customerShortName.value = ''
|
||
customerInvoiceName.value = ''
|
||
confirmedCustomerId.value = form.value.customerId || undefined
|
||
contactEmptyPrompted.value = false
|
||
if (form.value.customerId) {
|
||
try {
|
||
const [invoiceRes, customerRes] = await Promise.all([
|
||
getDefaultInvoiceInfoByCustomerId(form.value.customerId),
|
||
fetchCustomerDetail({ id: String(form.value.customerId) })
|
||
])
|
||
customerInvoiceName.value = invoiceRes?.data?.companyName || ''
|
||
customerShortName.value = customerRes?.data?.shortName || ''
|
||
if (!form.value.customerName && customerRes?.data?.customerName) {
|
||
form.value.customerName = customerRes.data.customerName
|
||
}
|
||
ensureCustomerOption({
|
||
id: form.value.customerId,
|
||
customerName: form.value.customerName || customerRes?.data?.customerName,
|
||
shortName: customerShortName.value,
|
||
customerNo: customerRes?.data?.customerNo
|
||
})
|
||
} catch {
|
||
customerInvoiceName.value = ''
|
||
ensureCustomerOption({
|
||
id: form.value.customerId,
|
||
customerName: form.value.customerName,
|
||
shortName: customerShortName.value
|
||
})
|
||
}
|
||
await loadContactOptions(form.value.customerId)
|
||
if (form.value.contactId) {
|
||
ensureContactOption({
|
||
id: form.value.contactId,
|
||
contactName: form.value.contactsName,
|
||
mobile: form.value.contactsPhone
|
||
})
|
||
}
|
||
} else {
|
||
customerOptions.value = []
|
||
contactOptions.value = []
|
||
}
|
||
syncPartiesSwappedFlagFromPanels()
|
||
rebuildContractName()
|
||
}
|
||
|
||
/** 选择客户 */
|
||
const handleSelectCustomer = () => {
|
||
customerSelectorVisible.value = true
|
||
}
|
||
|
||
/** 下拉选择客户 */
|
||
const handleCustomerSelectChange = async (customerId?: string | number) => {
|
||
if (!customerId) {
|
||
clearCustomerSelection()
|
||
return
|
||
}
|
||
if (
|
||
confirmedCustomerId.value !== undefined
|
||
&& confirmedCustomerId.value !== null
|
||
&& String(customerId) === String(confirmedCustomerId.value)
|
||
) {
|
||
return
|
||
}
|
||
const customer = customerOptions.value.find((item) => String(item.id) === String(customerId))
|
||
if (!customer) return
|
||
|
||
if (!hasCustomerShortName(customer)) {
|
||
ElMessage.warning('该客户尚未配置简称,请先填写客户简称后再继续')
|
||
form.value.customerId = confirmedCustomerId.value as any
|
||
openShortNameDialog(customer)
|
||
return
|
||
}
|
||
|
||
await handleCustomerConfirm(customer)
|
||
}
|
||
|
||
/** 客户选择器确认(无简称时先补填) */
|
||
const handleCustomerSelectorConfirm = async (customer: CustomerVO) => {
|
||
let enriched: CustomerVO = { ...customer }
|
||
if (!hasCustomerShortName(enriched)) {
|
||
try {
|
||
const customerRes = await fetchCustomerDetail({ id: String(customer.id) })
|
||
enriched = {
|
||
...enriched,
|
||
customerName: customerRes?.data?.customerName || enriched.customerName,
|
||
shortName: customerRes?.data?.shortName || enriched.shortName
|
||
}
|
||
} catch {
|
||
// ignore,后续走简称弹窗
|
||
}
|
||
}
|
||
if (!hasCustomerShortName(enriched)) {
|
||
ElMessage.warning('该客户尚未配置简称,请先填写客户简称后再继续')
|
||
openShortNameDialog(enriched)
|
||
return
|
||
}
|
||
await handleCustomerConfirm(enriched)
|
||
}
|
||
|
||
/** 客户选择确认 */
|
||
const handleCustomerConfirm = async (customer: CustomerVO) => {
|
||
if (customer) {
|
||
ensureCustomerOption(customer)
|
||
form.value.customerId = customer.id
|
||
form.value.customerName = customer.customerName
|
||
confirmedCustomerId.value = customer.id
|
||
|
||
const addressParts: string[] = []
|
||
if (customer.province) addressParts.push(customer.province)
|
||
if (customer.city) addressParts.push(customer.city)
|
||
if (customer.district) addressParts.push(customer.district)
|
||
if (customer.address) addressParts.push(customer.address)
|
||
const fullAddress = addressParts.join('')
|
||
|
||
form.value.contactId = undefined
|
||
form.value.contactsName = ''
|
||
contactOptions.value = []
|
||
contactEmptyPrompted.value = false
|
||
form.value.opportunityId = undefined
|
||
form.value.opportunityName = ''
|
||
form.value.quoteId = undefined
|
||
form.value.quoteNo = ''
|
||
form.value.freight = 0
|
||
form.value.barrelDeposit = 0
|
||
|
||
try {
|
||
const [invoiceRes, customerRes] = await Promise.all([
|
||
getDefaultInvoiceInfoByCustomerId(customer.id),
|
||
fetchCustomerDetail({ id: String(customer.id) })
|
||
])
|
||
const invoiceData = invoiceRes?.data
|
||
// 简称以客户主数据为准(列表/选择器可能缺字段)
|
||
customerShortName.value =
|
||
(customerRes?.data?.shortName || customer.shortName || '').trim()
|
||
if (customerRes?.data?.customerName) {
|
||
form.value.customerName = customerRes.data.customerName
|
||
}
|
||
ensureCustomerOption({
|
||
id: customer.id,
|
||
customerName: form.value.customerName,
|
||
shortName: customerShortName.value,
|
||
customerNo: customer.customerNo
|
||
})
|
||
|
||
customerInvoiceName.value = invoiceData?.companyName || ''
|
||
if (invoiceData && invoiceData.companyName) {
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyA = invoiceData.companyName || ''
|
||
form.value.partyAAddress = invoiceData.companyAddress || fullAddress || ''
|
||
form.value.partyALegalPerson = invoiceData.legalRepresentative || ''
|
||
form.value.partyAAgent = invoiceData.agent || ''
|
||
form.value.partyAPhoneFax = invoiceData.phoneFax || ''
|
||
form.value.partyABank = invoiceData.bankName || ''
|
||
form.value.partyAAccount = invoiceData.bankAccount || ''
|
||
} else {
|
||
form.value.partyB = invoiceData.companyName || ''
|
||
form.value.partyBAddress = invoiceData.companyAddress || fullAddress || ''
|
||
form.value.partyBLegalPerson = invoiceData.legalRepresentative || ''
|
||
form.value.partyBAgent = invoiceData.agent || ''
|
||
form.value.partyBPhoneFax = invoiceData.phoneFax || ''
|
||
form.value.partyBBank = invoiceData.bankName || ''
|
||
form.value.partyBAccount = invoiceData.bankAccount || ''
|
||
}
|
||
} else {
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyA = customer.customerName
|
||
form.value.partyAAddress = fullAddress
|
||
form.value.partyALegalPerson = ''
|
||
form.value.partyAAgent = ''
|
||
form.value.partyAPhoneFax = customer.telephone || ''
|
||
form.value.partyABank = ''
|
||
form.value.partyAAccount = ''
|
||
} else {
|
||
form.value.partyB = customer.customerName
|
||
form.value.partyBAddress = fullAddress
|
||
form.value.partyBLegalPerson = ''
|
||
form.value.partyBAgent = ''
|
||
form.value.partyBPhoneFax = customer.telephone || ''
|
||
form.value.partyBBank = ''
|
||
form.value.partyBAccount = ''
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取客户开票信息失败:', error)
|
||
customerInvoiceName.value = ''
|
||
customerShortName.value = (customer.shortName || '').trim()
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyA = customer.customerName
|
||
form.value.partyAAddress = fullAddress
|
||
form.value.partyALegalPerson = ''
|
||
form.value.partyAAgent = ''
|
||
form.value.partyAPhoneFax = customer.telephone || ''
|
||
form.value.partyABank = ''
|
||
form.value.partyAAccount = ''
|
||
} else {
|
||
form.value.partyB = customer.customerName
|
||
form.value.partyBAddress = fullAddress
|
||
form.value.partyBLegalPerson = ''
|
||
form.value.partyBAgent = ''
|
||
form.value.partyBPhoneFax = customer.telephone || ''
|
||
form.value.partyBBank = ''
|
||
form.value.partyBAccount = ''
|
||
}
|
||
}
|
||
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyAContact = ''
|
||
form.value.partyAMobile = customer.mobile || customer.telephone || ''
|
||
} else {
|
||
form.value.partyBContact = ''
|
||
form.value.partyBMobile = customer.mobile || customer.telephone || ''
|
||
}
|
||
form.value.contactsName = ''
|
||
form.value.contactsPhone = customer.mobile || customer.telephone || ''
|
||
|
||
// 自动加载该客户的联系人并设置默认联系人;无联系人时立即引导补充
|
||
const contacts = await loadContactOptions(customer.id)
|
||
if (contacts.length > 0) {
|
||
const primaryContact = contacts.find((c) => c.isPrimary === 1)
|
||
const contact = primaryContact || contacts[0]
|
||
if (contact) {
|
||
handleContactConfirm(contact)
|
||
}
|
||
} else {
|
||
contactEmptyPrompted.value = true
|
||
ElMessage.warning('该客户暂无联系人,请先补充联系人信息')
|
||
nextTick(() => openContactFormDialog())
|
||
}
|
||
|
||
if (!customerShortName.value) {
|
||
customerShortName.value = (customer.shortName || '').trim()
|
||
}
|
||
rebuildContractName()
|
||
}
|
||
}
|
||
|
||
const handleSaveShortName = async () => {
|
||
const valid = await shortNameFormRef.value?.validate().catch(() => false)
|
||
if (!valid) return
|
||
|
||
shortNameSaving.value = true
|
||
try {
|
||
const trimmedShortName = shortNameForm.value.shortName.trim()
|
||
await fetchUpdateCustomer({
|
||
id: shortNameForm.value.id,
|
||
shortName: trimmedShortName
|
||
})
|
||
ElMessage.success('客户简称保存成功')
|
||
|
||
const pending = pendingCustomerForShortName.value
|
||
const customer: CustomerVO = {
|
||
id: shortNameForm.value.id,
|
||
customerName: shortNameForm.value.customerName || pending?.customerName || '',
|
||
customerNo: pending?.customerNo,
|
||
shortName: trimmedShortName
|
||
} as CustomerVO
|
||
ensureCustomerOption(customer)
|
||
shortNameSavedSuccessfully.value = true
|
||
shortNameDialogVisible.value = false
|
||
await handleCustomerConfirm(customer)
|
||
} catch (error) {
|
||
console.error('保存客户简称失败:', error)
|
||
ElMessage.error('保存客户简称失败')
|
||
} finally {
|
||
shortNameSaving.value = false
|
||
}
|
||
}
|
||
|
||
const handleShortNameDialogClosed = () => {
|
||
pendingCustomerForShortName.value = null
|
||
if (!shortNameSavedSuccessfully.value) {
|
||
form.value.customerId = confirmedCustomerId.value as any
|
||
}
|
||
shortNameSavedSuccessfully.value = false
|
||
}
|
||
|
||
/** 选择联系人 */
|
||
const handleSelectContact = () => {
|
||
if (!form.value.customerId) {
|
||
$modal.msgWarning('请先选择客户')
|
||
return
|
||
}
|
||
contactSelectorVisible.value = true
|
||
}
|
||
|
||
const handleContactSelectVisible = async (visible: boolean) => {
|
||
if (!visible) {
|
||
contactEmptyPrompted.value = false
|
||
return
|
||
}
|
||
if (!form.value.customerId) return
|
||
|
||
const list = await loadContactOptions(form.value.customerId)
|
||
if (list.length === 0 && !contactEmptyPrompted.value) {
|
||
contactEmptyPrompted.value = true
|
||
ElMessage.warning('该客户暂无联系人,请先补充联系人信息')
|
||
nextTick(() => openContactFormDialog())
|
||
}
|
||
}
|
||
|
||
const handleContactSelectChange = (contactId?: string | number) => {
|
||
if (!contactId) {
|
||
form.value.contactId = undefined
|
||
form.value.contactsName = ''
|
||
form.value.contactsPhone = ''
|
||
return
|
||
}
|
||
const contact = contactOptions.value.find((item) => String(item.id) === String(contactId))
|
||
if (contact) {
|
||
handleContactConfirm(contact)
|
||
}
|
||
}
|
||
|
||
/** 联系人选择确认 */
|
||
const handleContactConfirm = (contact: ContactVO) => {
|
||
if (contact) {
|
||
ensureContactOption(contact)
|
||
form.value.contactId = contact.id
|
||
form.value.contactsName = contact.contactName
|
||
form.value.contactsPhone = contact.mobile || contact.telephone || ''
|
||
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyAContact = contact.contactName
|
||
form.value.partyAMobile = contact.mobile || contact.telephone || ''
|
||
} else {
|
||
form.value.partyBContact = contact.contactName
|
||
form.value.partyBMobile = contact.mobile || contact.telephone || ''
|
||
}
|
||
|
||
formRef.value?.validateField('contactId')
|
||
}
|
||
}
|
||
|
||
const handleContactFormSuccess = async (payload?: { id?: string | number }) => {
|
||
contactEmptyPrompted.value = false
|
||
const list = await loadContactOptions(form.value.customerId)
|
||
const createdId = payload?.id
|
||
if (createdId) {
|
||
const created = list.find((item) => String(item.id) === String(createdId))
|
||
if (created) {
|
||
handleContactConfirm(created)
|
||
return
|
||
}
|
||
}
|
||
if (list.length === 1) {
|
||
handleContactConfirm(list[0])
|
||
}
|
||
}
|
||
|
||
/** 选择商机 */
|
||
const handleSelectOpportunity = () => {
|
||
opportunitySelectorVisible.value = true
|
||
}
|
||
|
||
/** 商机选择确认 */
|
||
const handleOpportunityConfirm = (opportunity: OpportunityVO) => {
|
||
if (opportunity) {
|
||
form.value.opportunityId = opportunity.id
|
||
form.value.opportunityName = opportunity.opportunityName
|
||
|
||
if (opportunity.customerId && !form.value.customerId) {
|
||
form.value.customerId = opportunity.customerId
|
||
form.value.customerName = opportunity.customerName || ''
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 选择报价单 */
|
||
const handleSelectQuote = () => {
|
||
quoteSelectorVisible.value = true
|
||
}
|
||
|
||
/** 报价单选择确认 */
|
||
const handleQuoteConfirm = (quote: QuoteVO) => {
|
||
if (quote) {
|
||
form.value.quoteId = quote.id
|
||
form.value.quoteNo = quote.quoteNo
|
||
form.value.freight = Number(quote.freight) || 0
|
||
form.value.barrelDeposit = Number(quote.barrelDeposit) || 0
|
||
|
||
if (quote.customerId && !form.value.customerId) {
|
||
form.value.customerId = quote.customerId
|
||
form.value.customerName = quote.customerName || ''
|
||
}
|
||
|
||
if (quote.contactId) {
|
||
form.value.contactId = quote.contactId
|
||
form.value.contactsName = quote.contactName || ''
|
||
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyAContact = quote.contactName || ''
|
||
} else {
|
||
form.value.partyBContact = quote.contactName || ''
|
||
}
|
||
}
|
||
|
||
if (quote.productList && quote.productList.length > 0) {
|
||
$modal
|
||
.confirm('是否要从报价单中带入产品明细?')
|
||
.then(async () => {
|
||
const isFw = isFrameworkContractType(form.value.contractType)
|
||
form.value.products = quote.productList!.map((item: any) => {
|
||
const hasQuotePrice = Number(item.price) > 0 || Number(item.subtotal) > 0
|
||
const hasQuoteQty = Number(item.quantity) > 0
|
||
const fwDefaults = createFrameworkProductPricingDefaults()
|
||
return {
|
||
productId: item.productId,
|
||
productName: item.productName,
|
||
productModel: item.model,
|
||
categoryId: item.categoryId,
|
||
categoryName: item.categoryName || '',
|
||
unit: item.unit || '个',
|
||
...(isFw
|
||
? {
|
||
...fwDefaults,
|
||
quantityMode: hasQuoteQty ? 'fixed' : fwDefaults.quantityMode,
|
||
quantity: hasQuoteQty ? item.quantity || 1 : 0,
|
||
priceMode: hasQuotePrice ? 'fixed' : fwDefaults.priceMode,
|
||
price: hasQuotePrice ? item.price || 0 : 0,
|
||
taxAmount: hasQuotePrice ? item.taxAmount || 0 : 0,
|
||
subtotal: hasQuotePrice
|
||
? item.subtotal || (item.quantity || 1) * (item.price || 0) * (1 - (item.discount || 0) / 100)
|
||
: 0
|
||
}
|
||
: {
|
||
quantity: item.quantity || 1,
|
||
price: item.price || 0,
|
||
unitPrice: item.price || 0,
|
||
taxAmount: item.taxAmount || 0,
|
||
subtotal:
|
||
item.subtotal || (item.quantity || 1) * (item.price || 0) * (1 - (item.discount || 0) / 100)
|
||
}),
|
||
taxRate: item.taxRate || 13,
|
||
discount: item.discount || 0,
|
||
packageSpec: item.packageSpec || '',
|
||
deliveryMethod: item.deliveryMethod || '',
|
||
deliveryDate: '',
|
||
description: item.description || '',
|
||
remark: item.remark || '',
|
||
sort: item.sort,
|
||
brandMode: item.brandMode || undefined,
|
||
brandId: item.brandId || undefined,
|
||
brandName: item.brandName || undefined,
|
||
brandOptions: [],
|
||
priceTiers: isFw ? [] : undefined
|
||
}
|
||
})
|
||
await enrichContractProductBrandMeta(form.value.products)
|
||
form.value.products.forEach((p) => calculateProductAmount(p))
|
||
calculateTotalAmount()
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 加载用户列表(负责人下拉) */
|
||
const loadUserList = async () => {
|
||
try {
|
||
const res = await fetchUserOptions()
|
||
const options = (res as any)?.data ?? res ?? []
|
||
userList.value = (Array.isArray(options) ? options : []).map((item: any) => ({
|
||
userId: item.value ?? item.userId ?? item.id,
|
||
nickName: item.label ?? item.nickName ?? item.userName ?? ''
|
||
}))
|
||
} catch (error) {
|
||
console.error('加载用户列表失败:', error)
|
||
userList.value = []
|
||
}
|
||
}
|
||
|
||
/** 新增时默认负责人为方金强 */
|
||
const applyDefaultOwnerUser = () => {
|
||
const matched = findDefaultOwnerUser(ownerUserList.value)
|
||
if (!matched) return
|
||
form.value.ownerUserId = matched.userId as ContractForm['ownerUserId']
|
||
form.value.ownerUserName = matched.nickName || ''
|
||
}
|
||
|
||
/** 负责人变化 */
|
||
const handleOwnerChange = (userId: string | number) => {
|
||
const user = ownerUserList.value.find((item: any) => String(item.userId) === String(userId))
|
||
form.value.ownerUserName = user?.nickName || ''
|
||
}
|
||
|
||
/** 选择负责人 */
|
||
const handleSelectUser = () => {
|
||
userSelectorVisible.value = true
|
||
}
|
||
|
||
/** 用户选择确认 */
|
||
const handleUserConfirm = (user: UserVO) => {
|
||
if (user) {
|
||
form.value.ownerUserId = user.userId
|
||
form.value.ownerUserName = user.nickName
|
||
}
|
||
}
|
||
|
||
/** 供方主体变化处理 */
|
||
const handleSupplierChange = async (supplierId: number | undefined) => {
|
||
if (!supplierId) {
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyB = ''
|
||
form.value.partyBAddress = ''
|
||
form.value.partyBLegalPerson = ''
|
||
form.value.partyBAgent = ''
|
||
form.value.partyBPhoneFax = ''
|
||
form.value.partyBBank = ''
|
||
form.value.partyBAccount = ''
|
||
} else {
|
||
form.value.partyA = ''
|
||
form.value.partyAAddress = ''
|
||
form.value.partyALegalPerson = ''
|
||
form.value.partyAAgent = ''
|
||
form.value.partyAPhoneFax = ''
|
||
form.value.partyABank = ''
|
||
form.value.partyAAccount = ''
|
||
}
|
||
return
|
||
}
|
||
|
||
const supplier = supplierList.value.find((s) => s.id === supplierId)
|
||
if (supplier) {
|
||
if (isPartiesSwapped.value) {
|
||
form.value.partyB = supplier.supplierName || ''
|
||
form.value.partyBAddress = supplier.supplierAddress || ''
|
||
form.value.partyBLegalPerson = supplier.legalPerson || ''
|
||
form.value.partyBAgent = supplier.agent || ''
|
||
form.value.partyBPhoneFax = supplier.phoneFax || ''
|
||
form.value.partyBBank = supplier.bankName || ''
|
||
form.value.partyBAccount = supplier.bankAccount || ''
|
||
} else {
|
||
form.value.partyA = supplier.supplierName || ''
|
||
form.value.partyAAddress = supplier.supplierAddress || ''
|
||
form.value.partyALegalPerson = supplier.legalPerson || ''
|
||
form.value.partyAAgent = supplier.agent || ''
|
||
form.value.partyAPhoneFax = supplier.phoneFax || ''
|
||
form.value.partyABank = supplier.bankName || ''
|
||
form.value.partyAAccount = supplier.bankAccount || ''
|
||
}
|
||
|
||
updateContractNoBySupplier(supplier.supplierCode)
|
||
rebuildContractName()
|
||
|
||
const allowToxic = Number((supplier as any).allowToxic || 0)
|
||
const allowExplosive = Number((supplier as any).allowExplosive || 0)
|
||
const { kept, removedNames } = removeProductsNotAllowedByHazard(
|
||
form.value.products || [],
|
||
allowToxic,
|
||
allowExplosive
|
||
)
|
||
form.value.products = kept
|
||
if (removedNames.length) {
|
||
$modal.msgWarning(`当前主体不支持以下产品,已自动移除:${removedNames.join('、')}`)
|
||
form.value.products.forEach((p) => calculateProductAmount(p))
|
||
calculateTotalAmount()
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 更新合同编号中的供方编码 */
|
||
const updateContractNoBySupplier = (newSupplierCode: string | undefined) => {
|
||
if (!form.value.contractNo || !newSupplierCode) return
|
||
|
||
const contractNoPattern = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/
|
||
const match = form.value.contractNo.match(contractNoPattern)
|
||
|
||
if (match) {
|
||
const date = match[2]
|
||
const serialNum = match[3]
|
||
form.value.contractNo = `${newSupplierCode}/X-JT${date}${serialNum}`
|
||
} else {
|
||
const index = form.value.contractNo.indexOf('/X-JT')
|
||
if (index > 0) {
|
||
const oldSupplierCode = form.value.contractNo.substring(0, index)
|
||
form.value.contractNo = form.value.contractNo.replace(oldSupplierCode, newSupplierCode)
|
||
}
|
||
}
|
||
}
|
||
|
||
const CONTRACT_NO_STD_RE = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/
|
||
|
||
/** 签订日变更时:标准编号只换日期段,保留序号;并同步名称括号内编号 */
|
||
const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
|
||
if (!signDateVal || !form.value.contractNo) return
|
||
const m = String(form.value.contractNo).match(CONTRACT_NO_STD_RE)
|
||
if (!m) return
|
||
|
||
const dateStr = signDateVal.replace(/-/g, '')
|
||
if (dateStr.length !== 8) return
|
||
if (m[2] === dateStr) return
|
||
|
||
const nextNo = `${m[1]}/X-JT${dateStr}${m[3]}`
|
||
form.value.contractNo = nextNo
|
||
rebuildContractName()
|
||
}
|
||
|
||
/** 面板单位全称 → 匹配交易主体/客户主数据取简称;对不上用全称 */
|
||
const resolvePartyShortName = (partyName?: string): string => {
|
||
const party = (partyName || '').trim()
|
||
if (!party) return ''
|
||
|
||
const supplier = form.value.supplierId ? supplierList.value.find((s) => s.id === form.value.supplierId) : undefined
|
||
const supplierName = (supplier?.supplierName || '').trim()
|
||
const supplierShort = (supplier?.shortName || supplierName || '').trim()
|
||
if (supplierName && party === supplierName) return supplierShort || party
|
||
if (supplierShort && party === supplierShort) return supplierShort
|
||
|
||
const customerName = (form.value.customerName || '').trim()
|
||
const customerShort = (customerShortName.value || '').trim()
|
||
const invoiceName = (customerInvoiceName.value || '').trim()
|
||
if (customerName && party === customerName) return customerShort || customerName
|
||
if (customerShort && party === customerShort) return customerShort
|
||
if (invoiceName && party === invoiceName) return customerShort || invoiceName
|
||
|
||
return party
|
||
}
|
||
|
||
/** 重建合同名称:始终 {供方简称}-{需方简称}({合同编号}),随面板调换 */
|
||
const rebuildContractName = () => {
|
||
const partyAShort = resolvePartyShortName(form.value.partyA)
|
||
const partyBShort = resolvePartyShortName(form.value.partyB)
|
||
const nameBody = [partyAShort, partyBShort].filter(Boolean).join('-')
|
||
if (!nameBody) return
|
||
|
||
const contractNo = (form.value.contractNo || '').trim()
|
||
form.value.contractName = contractNo ? `${nameBody}(${contractNo})` : nameBody
|
||
}
|
||
|
||
/** 根据面板与交易主体/客户推断是否已调换(编辑回显用) */
|
||
const syncPartiesSwappedFlagFromPanels = () => {
|
||
const partyA = (form.value.partyA || '').trim()
|
||
const partyB = (form.value.partyB || '').trim()
|
||
if (!partyA && !partyB) {
|
||
isPartiesSwapped.value = false
|
||
return
|
||
}
|
||
|
||
const supplier = form.value.supplierId
|
||
? supplierList.value.find((s) => s.id === form.value.supplierId)
|
||
: undefined
|
||
const supplierName = (supplier?.supplierName || '').trim()
|
||
|
||
if (supplierName) {
|
||
if (partyB === supplierName && partyA !== supplierName) {
|
||
isPartiesSwapped.value = true
|
||
return
|
||
}
|
||
if (partyA === supplierName) {
|
||
isPartiesSwapped.value = false
|
||
return
|
||
}
|
||
}
|
||
|
||
const customerName = (form.value.customerName || '').trim()
|
||
const invoiceName = (customerInvoiceName.value || '').trim()
|
||
if ((customerName && partyA === customerName) || (invoiceName && partyA === invoiceName)) {
|
||
isPartiesSwapped.value = true
|
||
return
|
||
}
|
||
if ((customerName && partyB === customerName) || (invoiceName && partyB === invoiceName)) {
|
||
isPartiesSwapped.value = false
|
||
}
|
||
}
|
||
|
||
/** 供需方调换:仅交换面板字段,勿再调 handleSupplierChange(会按交易主体覆盖一侧,导致两边都变成交易主体) */
|
||
const handleSwapParties = async () => {
|
||
const temp = {
|
||
partyA: form.value.partyA,
|
||
partyAAddress: form.value.partyAAddress,
|
||
partyAContact: form.value.partyAContact,
|
||
partyAMobile: form.value.partyAMobile,
|
||
partyALegalPerson: form.value.partyALegalPerson,
|
||
partyAAgent: form.value.partyAAgent,
|
||
partyAPhoneFax: form.value.partyAPhoneFax,
|
||
partyABank: form.value.partyABank,
|
||
partyAAccount: form.value.partyAAccount
|
||
}
|
||
form.value.partyA = form.value.partyB
|
||
form.value.partyAAddress = form.value.partyBAddress
|
||
form.value.partyAContact = form.value.partyBContact
|
||
form.value.partyAMobile = form.value.partyBMobile
|
||
form.value.partyALegalPerson = form.value.partyBLegalPerson
|
||
form.value.partyAAgent = form.value.partyBAgent
|
||
form.value.partyAPhoneFax = form.value.partyBPhoneFax
|
||
form.value.partyABank = form.value.partyBBank
|
||
form.value.partyAAccount = form.value.partyBAccount
|
||
|
||
form.value.partyB = temp.partyA
|
||
form.value.partyBAddress = temp.partyAAddress
|
||
form.value.partyBContact = temp.partyAContact
|
||
form.value.partyBMobile = temp.partyAMobile
|
||
form.value.partyBLegalPerson = temp.partyALegalPerson
|
||
form.value.partyBAgent = temp.partyAAgent
|
||
form.value.partyBPhoneFax = temp.partyAPhoneFax
|
||
form.value.partyBBank = temp.partyABank
|
||
form.value.partyBAccount = temp.partyAAccount
|
||
|
||
isPartiesSwapped.value = !isPartiesSwapped.value
|
||
rebuildContractName()
|
||
|
||
$modal.msgSuccess('供需方信息已调换')
|
||
}
|
||
|
||
/** 合同类型变化处理 */
|
||
const handleContractTypeChange = (value: string) => {
|
||
if (isFrameworkContractType(value)) {
|
||
form.value.amount = 0
|
||
form.value.products?.forEach((p) => {
|
||
applyFrameworkDefaultsToProduct(p)
|
||
if (!p.priceTiers) {
|
||
p.priceTiers = []
|
||
}
|
||
})
|
||
} else {
|
||
form.value.products?.forEach((p) => {
|
||
normalizeFrameworkProductPricing(p, false)
|
||
calculateProductAmount(p)
|
||
})
|
||
calculateTotalAmount()
|
||
}
|
||
if (isPurchaseLikeContractType(value) && !isPartiesSwapped.value) {
|
||
$modal
|
||
.confirm('检测到您选择了采购类合同,系统将自动调换供需方信息,是否继续?')
|
||
.then(async () => {
|
||
await handleSwapParties()
|
||
})
|
||
.catch(() => {
|
||
rebuildContractName()
|
||
})
|
||
} else {
|
||
rebuildContractName()
|
||
}
|
||
}
|
||
|
||
/** 合同模版变化处理 */
|
||
const handleContractTemplateChange = (value: string) => {
|
||
if (value === 'customer_template') {
|
||
ElMessage({
|
||
message: '使用客户模版时,必须上传合同附件!',
|
||
type: 'warning',
|
||
duration: 3000
|
||
})
|
||
}
|
||
}
|
||
|
||
/** 合同期限日期范围变化 */
|
||
const handleDateRangeChange = (val: [string, string] | null) => {
|
||
if (val && val.length === 2) {
|
||
form.value.startDate = val[0]
|
||
form.value.endDate = val[1]
|
||
} else {
|
||
form.value.startDate = ''
|
||
form.value.endDate = ''
|
||
contractDateRange.value = [] as any
|
||
}
|
||
nextTick(() => {
|
||
formRef.value?.validateField('startDate')
|
||
})
|
||
}
|
||
|
||
/** 签订日期变化处理 */
|
||
const handleSignDateChange = (val: string | null) => {
|
||
if (val) {
|
||
const signDate = new Date(val)
|
||
const endDate = new Date(signDate)
|
||
endDate.setDate(endDate.getDate() + 30)
|
||
|
||
const formatDate = (date: Date): string => {
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
return `${year}-${month}-${day}`
|
||
}
|
||
|
||
const startDateStr = formatDate(signDate)
|
||
const endDateStr = formatDate(endDate)
|
||
contractDateRange.value = [startDateStr, endDateStr]
|
||
form.value.startDate = startDateStr
|
||
form.value.endDate = endDateStr
|
||
} else {
|
||
contractDateRange.value = [] as any
|
||
form.value.startDate = ''
|
||
form.value.endDate = ''
|
||
}
|
||
syncContractNoDateFromSignDate(val)
|
||
nextTick(() => {
|
||
formRef.value?.validateField('startDate')
|
||
})
|
||
}
|
||
|
||
/** 添加产品 */
|
||
const handleAddProduct = () => {
|
||
if (!form.value.supplierId) {
|
||
$modal.msgWarning('请先选择交易主体')
|
||
return
|
||
}
|
||
productSelectorVisible.value = true
|
||
}
|
||
|
||
/** 产品选择确认 */
|
||
const handleProductSelected = (products: any[]) => {
|
||
const isFw = isFrameworkContractType(form.value.contractType)
|
||
products.forEach((product) => {
|
||
const brandMeta = initLineBrandMetaFromProduct(product)
|
||
form.value.products.push({
|
||
productId: product.id,
|
||
productName: product.productName,
|
||
productModel: product.model || '',
|
||
categoryId: product.categoryId,
|
||
categoryName: product.categoryName || '',
|
||
unit: product.unit || '个',
|
||
...(isFw ? createFrameworkProductPricingDefaults() : { quantity: 1, price: product.price || 0, taxAmount: 0, subtotal: product.price || 0 }),
|
||
taxRate: normalizeTaxRateDisplay(product.taxRate, 13),
|
||
packageSpec: '',
|
||
deliveryMethod: '',
|
||
description: product.description || '',
|
||
remark: '',
|
||
isToxic: Number(product.isToxic || 0),
|
||
isExplosive: Number(product.isExplosive || 0),
|
||
...brandMeta,
|
||
priceTiers: isFw ? [] : undefined
|
||
})
|
||
})
|
||
form.value.products.forEach((p) => {
|
||
calculateProductAmount(p)
|
||
})
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
const handleBrandIdChange = (row: ExtendedContractProductForm, val: string | undefined | null) => {
|
||
applyBrandSelectChange(row, val, row.brandOptions || [])
|
||
}
|
||
|
||
const aliasBrandManageVisible = ref(false)
|
||
const aliasBrandManageProductId = ref('')
|
||
const aliasBrandManageProductName = ref('')
|
||
const aliasBrandManageMode = ref<'alias' | 'brand'>('alias')
|
||
|
||
function openAliasBrandManage(row: ExtendedContractProductForm, mode: 'alias' | 'brand') {
|
||
if (!row.productId) return
|
||
aliasBrandManageProductId.value = String(row.productId)
|
||
aliasBrandManageProductName.value = String(
|
||
row.standardProductName || row.productName || ''
|
||
)
|
||
aliasBrandManageMode.value = mode
|
||
aliasBrandManageVisible.value = true
|
||
}
|
||
|
||
function handleAliasBrandManaged(product: Record<string, any>) {
|
||
applyManagedProductMetaToLines(
|
||
form.value.products || [],
|
||
aliasBrandManageProductId.value,
|
||
product
|
||
)
|
||
}
|
||
|
||
async function enrichContractProductBrandMeta(lines: ExtendedContractProductForm[]) {
|
||
await Promise.all(
|
||
(lines || []).map(async (line) => {
|
||
if (!line.productId) return
|
||
try {
|
||
const res: any = await fetchProductDetail({ id: String(line.productId) })
|
||
const data = res?.data ?? res
|
||
if (!data) return
|
||
const meta = initLineBrandMetaFromProduct(data)
|
||
line.standardProductName = meta.standardProductName
|
||
line.aliasOptions = meta.aliasOptions
|
||
line.aliasName = resolveAliasFromSavedName(
|
||
line.productName,
|
||
meta.standardProductName,
|
||
meta.aliasOptions
|
||
)
|
||
line.brandOptions = meta.brandOptions
|
||
if (meta.brandOptions.length) {
|
||
if (line.brandMode === 'required' && line.brandId) {
|
||
applyBrandSelectChange(line, line.brandId, meta.brandOptions)
|
||
} else {
|
||
line.brandMode = 'any'
|
||
line.brandId = undefined
|
||
line.brandName = undefined
|
||
}
|
||
} else {
|
||
line.brandMode = undefined
|
||
line.brandId = undefined
|
||
line.brandName = undefined
|
||
}
|
||
} catch (e) {
|
||
console.warn('enrich contract brand meta failed', line.productId, e)
|
||
}
|
||
})
|
||
)
|
||
}
|
||
|
||
/** 规范化税率展示 */
|
||
const normalizeTaxRateDisplay = (value: any, defaultValue = 0): number => {
|
||
if (value === null || value === undefined || value === '') {
|
||
return defaultValue
|
||
}
|
||
const num = Number(value)
|
||
if (Number.isNaN(num)) {
|
||
return defaultValue
|
||
}
|
||
return Number.isInteger(num) ? num : parseFloat(num.toString())
|
||
}
|
||
|
||
/** 产品选择变化 */
|
||
const handleProductSelectionChange = (selection: ExtendedContractProductForm[]) => {
|
||
selectedProducts.value = selection
|
||
}
|
||
|
||
/** 删除选中的产品 */
|
||
const handleRemoveSelectedProducts = () => {
|
||
if (selectedProducts.value.length === 0) return
|
||
|
||
$modal
|
||
.confirm(`确认删除选中的 ${selectedProducts.value.length} 个产品吗?`)
|
||
.then(() => {
|
||
selectedProducts.value.forEach((selectedProduct) => {
|
||
const index = form.value.products.findIndex((p) => p === selectedProduct)
|
||
if (index > -1) {
|
||
form.value.products.splice(index, 1)
|
||
}
|
||
})
|
||
selectedProducts.value = []
|
||
calculateTotalAmount()
|
||
$modal.msgSuccess('删除成功')
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
|
||
/** 删除产品 */
|
||
const handleRemoveProduct = (index: number) => {
|
||
form.value.products.splice(index, 1)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
/** 单价变化 */
|
||
const handlePriceChange = (row: ExtendedContractProductForm) => {
|
||
calculateProductAmount(row)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
/** 数量变化 */
|
||
const handleQuantityChange = (row: ExtendedContractProductForm) => {
|
||
calculateProductAmount(row)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
/** 税率变化 */
|
||
const handleTaxRateChange = (row: ExtendedContractProductForm) => {
|
||
calculateProductAmount(row)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
const handleEnableFrameworkQuantityFixed = (row: ExtendedContractProductForm) => {
|
||
enableFrameworkQuantityFixed(row)
|
||
calculateProductAmount(row)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
const handleResetFrameworkQuantity = (row: ExtendedContractProductForm) => {
|
||
resetFrameworkQuantityToCustomerDemand(row)
|
||
calculateProductAmount(row)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
const handleEnableFrameworkPriceFixed = (row: ExtendedContractProductForm) => {
|
||
enableFrameworkPriceFixed(row)
|
||
calculateProductAmount(row)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
const handleResetFrameworkPrice = (row: ExtendedContractProductForm) => {
|
||
resetFrameworkPriceToMarket(row)
|
||
calculateTotalAmount()
|
||
}
|
||
|
||
/** 计算产品金额 */
|
||
const calculateProductAmount = (product: ExtendedContractProductForm) => {
|
||
if (isFrameworkContractType(form.value.contractType) && isFrameworkPriceMarket(product)) {
|
||
product.price = 0
|
||
product.subtotal = 0
|
||
product.taxAmount = 0
|
||
return
|
||
}
|
||
const price = parseFloat(product.price?.toString() || '0')
|
||
const quantity = parseFloat(product.quantity?.toString() || '0')
|
||
const taxRate = parseFloat(product.taxRate?.toString() || '0')
|
||
|
||
const subtotal = Math.round(price * quantity * 100) / 100
|
||
product.subtotal = subtotal
|
||
|
||
const taxRateDecimal = taxRate / 100
|
||
let taxAmount = 0
|
||
if (taxRate > 0 && subtotal > 0) {
|
||
const taxBase = subtotal / (1 + taxRateDecimal)
|
||
taxAmount = Math.round(taxBase * taxRateDecimal * 100) / 100
|
||
}
|
||
product.taxAmount = taxAmount
|
||
}
|
||
|
||
/** 计算合计金额 */
|
||
const calculateTotalAmount = () => {
|
||
if (isFrameworkContractType(form.value.contractType)) {
|
||
form.value.amount = 0
|
||
return
|
||
}
|
||
let total = 0
|
||
form.value.products.forEach((item) => {
|
||
const quantity = item.quantity || 0
|
||
const price = item.price || 0
|
||
const discount = item.discount || 0
|
||
const subtotal = quantity * price * (1 - discount / 100)
|
||
;(item as ExtendedContractProductForm).subtotal = subtotal
|
||
total += subtotal
|
||
})
|
||
form.value.amount = total
|
||
}
|
||
|
||
/** 框架合同调价函:逗号分隔 URL 与 letterOssIds 互转 */
|
||
const tierLetterCsv = (row: PriceTierRow) => (row.letterOssIds || []).join(',')
|
||
|
||
const syncTierLetterCsv = (row: PriceTierRow, val: string) => {
|
||
row.letterOssIds = String(val || '')
|
||
.split(',')
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
}
|
||
|
||
/** 组装提交/保存的产品明细 */
|
||
const buildProductsPayload = () => {
|
||
const list = form.value.products || []
|
||
list.forEach((product) => prepareLineAliasAndBrandForSubmit(product))
|
||
return list.map((product: any, idx: number) => {
|
||
const base: any = {
|
||
contractId: product.contractId || form.value.id,
|
||
productId: product.productId,
|
||
productName: product.productName,
|
||
productNo: product.productNo,
|
||
categoryId: product.categoryId,
|
||
categoryName: product.categoryName,
|
||
model: product.productModel || product.model,
|
||
unit: product.unit,
|
||
quantity: product.quantity,
|
||
price: product.price,
|
||
discount: product.discount,
|
||
taxRate: product.taxRate,
|
||
taxAmount: product.taxAmount,
|
||
subtotal: product.subtotal,
|
||
deliveryDate: product.deliveryDate,
|
||
description: product.description,
|
||
packageSpec: product.packageSpec,
|
||
deliveryMethod: product.deliveryMethod,
|
||
sort: product.sort ?? idx + 1,
|
||
remark: product.remark,
|
||
brandMode: product.brandMode || null,
|
||
brandId: product.brandMode === 'required' ? product.brandId || null : null,
|
||
brandName: product.brandMode === 'required' ? product.brandName || null : null
|
||
}
|
||
if (product.id) {
|
||
base.id = product.id
|
||
}
|
||
if (isFrameworkContractType(form.value.contractType)) {
|
||
const fwPricing = prepareFrameworkProductForSubmit(product)
|
||
Object.assign(base, fwPricing)
|
||
base.priceTiers = (product.priceTiers || [])
|
||
.filter(
|
||
(t: any) => t.startDate && t.endDate && t.price != null && t.price !== '' && Array.isArray(t.letterOssIds) && t.letterOssIds.length > 0
|
||
)
|
||
.map((t: any, i: number) => ({
|
||
id: t.id,
|
||
startDate: t.startDate,
|
||
endDate: t.endDate,
|
||
price: Number(t.price),
|
||
letterOssIds: (t.letterOssIds || []).map((id: string) => String(id).trim()).filter(Boolean),
|
||
sort: t.sort ?? i + 1
|
||
}))
|
||
}
|
||
return base
|
||
})
|
||
}
|
||
|
||
const addPriceTierRow = (pIdx: number) => {
|
||
const p = form.value.products[pIdx]
|
||
if (!p) return
|
||
if (!p.priceTiers) {
|
||
p.priceTiers = []
|
||
}
|
||
p.priceTiers.push({ startDate: '', endDate: '', price: undefined, letterOssIds: [] })
|
||
}
|
||
|
||
const removePriceTierRow = (pIdx: number, tIdx: number) => {
|
||
const p = form.value.products[pIdx]
|
||
if (!p?.priceTiers) return
|
||
p.priceTiers.splice(tIdx, 1)
|
||
}
|
||
|
||
/** 处理附件数据 */
|
||
const handleAttachmentData = async () => {
|
||
attachmentList.value = buildUploadFileListFromField(
|
||
form.value.attachmentFiles
|
||
) as ContractAttachmentItem[]
|
||
}
|
||
|
||
/** 保存草稿 */
|
||
const handleSaveDraft = async () => {
|
||
if (!form.value.customerId) {
|
||
$modal.msgWarning('客户名称不能为空')
|
||
return
|
||
}
|
||
if (!form.value.supplierId) {
|
||
$modal.msgWarning('交易主体不能为空')
|
||
return
|
||
}
|
||
|
||
const confirmedZero = await confirmZeroPricingIfNeeded(
|
||
form.value.products,
|
||
isFrameworkContractType(form.value.contractType)
|
||
)
|
||
if (!confirmedZero) return
|
||
|
||
rebuildContractName()
|
||
saveDraftLoading.value = true
|
||
try {
|
||
const attachmentJson = serializeContractAttachments(attachmentList.value)
|
||
const data: any = { ...form.value }
|
||
data.status = 'draft'
|
||
data.approvalStatus = null
|
||
|
||
if (attachmentJson) {
|
||
data.attachmentFiles = attachmentJson
|
||
}
|
||
|
||
omitEmptyExceptPartyFields(data)
|
||
|
||
if (data.contactsName) {
|
||
data.contactName = data.contactsName
|
||
}
|
||
if (data.contactsPhone) {
|
||
data.contactMobile = data.contactsPhone
|
||
}
|
||
|
||
delete data.customerName
|
||
delete data.opportunityName
|
||
delete data.quoteNo
|
||
delete data.contactsName
|
||
delete data.contactsPhone
|
||
delete data.ownerUserName
|
||
delete data.templateContent
|
||
|
||
if (data.products && Array.isArray(data.products) && data.products.length > 0) {
|
||
;(form.value.products || []).forEach((p) => prepareLineAliasAndBrandForSubmit(p))
|
||
for (let i = 0; i < form.value.products.length; i++) {
|
||
const product = form.value.products[i]
|
||
const rowNum = i + 1
|
||
if (!product.productName || product.productName.trim() === '') {
|
||
$modal.msgWarning(`第${rowNum}行产品的产品名称不能为空`)
|
||
return
|
||
}
|
||
}
|
||
data.products = buildProductsPayload()
|
||
} else {
|
||
delete data.products
|
||
}
|
||
|
||
if (form.value.id) {
|
||
await updateContract(data)
|
||
$modal.msgSuccess('保存成功')
|
||
} else {
|
||
await addContract(data)
|
||
$modal.msgSuccess('保存成功')
|
||
}
|
||
handleClose()
|
||
emit('success')
|
||
} finally {
|
||
saveDraftLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 提交审批 */
|
||
const handleSubmit = async () => {
|
||
try {
|
||
await formRef.value?.validate()
|
||
} catch (error) {
|
||
expandPartiesForInvalidFields(error as Record<string, unknown>)
|
||
ElMessage.warning({
|
||
message: '请填写完整的必填信息',
|
||
duration: 3000
|
||
})
|
||
return
|
||
}
|
||
|
||
if (form.value.contractTemplate === 'customer') {
|
||
const hasAttachments = attachmentList.value.some((file) => file.status === 'success' && file.ossId)
|
||
if (!hasAttachments) {
|
||
ElMessage.warning({
|
||
message: '使用客户模版时,必须上传合同附件',
|
||
duration: 3000
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || isFrameworkContractType(form.value.contractType)
|
||
|
||
const validateProductRowCommon = (product: any, rowNum: number): boolean => {
|
||
if (!product.productName || String(product.productName).trim() === '') {
|
||
$modal.msgWarning(`第${rowNum}行产品的产品名称不能为空`)
|
||
return false
|
||
}
|
||
const isFwRow = isFrameworkContractType(form.value.contractType)
|
||
if (isFwRow) {
|
||
if (product.quantityMode === 'fixed') {
|
||
if (isBlankNumeric(product.quantity)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的数量不能为空`)
|
||
return false
|
||
}
|
||
if (isNegativeNumeric(product.quantity)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的数量不能为负数`)
|
||
return false
|
||
}
|
||
}
|
||
} else if (isBlankNumeric(product.quantity)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的数量不能为空`)
|
||
return false
|
||
} else if (isNegativeNumeric(product.quantity)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的数量不能为负数`)
|
||
return false
|
||
}
|
||
if (!product.packageSpec || String(product.packageSpec).trim() === '') {
|
||
$modal.msgWarning(`第${rowNum}行产品的包装规格不能为空`)
|
||
return false
|
||
}
|
||
if (!product.deliveryMethod || String(product.deliveryMethod).trim() === '') {
|
||
$modal.msgWarning(`第${rowNum}行产品的送货方式不能为空`)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
if (requiresProducts) {
|
||
if (!form.value.products || form.value.products.length === 0) {
|
||
$modal.msgWarning('销售合同、采购合同或框架合同必须至少添加一个产品')
|
||
return
|
||
}
|
||
;(form.value.products || []).forEach((p) => prepareLineAliasAndBrandForSubmit(p))
|
||
for (let i = 0; i < form.value.products.length; i++) {
|
||
const product = form.value.products[i]
|
||
const rowNum = i + 1
|
||
if (!validateProductRowCommon(product, rowNum)) return
|
||
const isFw = isFrameworkContractType(form.value.contractType)
|
||
if (!isFw) {
|
||
if (isBlankNumeric(product.price)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空`)
|
||
return
|
||
}
|
||
if (isNegativeNumeric(product.price) || isNegativeNumeric(product.subtotal) || isNegativeNumeric(product.taxAmount)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的含税单价、含税总价、税费不能为负数`)
|
||
return
|
||
}
|
||
} else {
|
||
if (product.priceMode === 'fixed') {
|
||
if (isBlankNumeric(product.price)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空`)
|
||
return
|
||
}
|
||
if (isNegativeNumeric(product.price) || isNegativeNumeric(product.subtotal) || isNegativeNumeric(product.taxAmount)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的含税单价、含税总价、税费不能为负数`)
|
||
return
|
||
}
|
||
}
|
||
const tiers = product.priceTiers || []
|
||
for (let j = 0; j < tiers.length; j++) {
|
||
const t = tiers[j]
|
||
const tRow = j + 1
|
||
if (!t.startDate || !t.endDate) {
|
||
$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:请选择开始与结束日期`)
|
||
return
|
||
}
|
||
if (String(t.startDate) > String(t.endDate)) {
|
||
$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:开始日期不能晚于结束日期`)
|
||
return
|
||
}
|
||
const pVal = t.price === null || t.price === '' ? NaN : Number(t.price)
|
||
if (Number.isNaN(pVal) || pVal <= 0) {
|
||
$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:含税单价必须大于0`)
|
||
return
|
||
}
|
||
if (!(t.letterOssIds?.length > 0)) {
|
||
$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:请上传调价函`)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if (form.value.products && form.value.products.length > 0) {
|
||
;(form.value.products || []).forEach((p) => prepareLineAliasAndBrandForSubmit(p))
|
||
for (let i = 0; i < form.value.products.length; i++) {
|
||
const product = form.value.products[i]
|
||
const rowNum = i + 1
|
||
if (!validateProductRowCommon(product, rowNum)) return
|
||
if (isBlankNumeric(product.price)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空`)
|
||
return
|
||
}
|
||
if (isNegativeNumeric(product.price) || isNegativeNumeric(product.subtotal) || isNegativeNumeric(product.taxAmount)) {
|
||
$modal.msgWarning(`第${rowNum}行产品的含税单价、含税总价、税费不能为负数`)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
const confirmedZero = await confirmZeroPricingIfNeeded(
|
||
form.value.products,
|
||
isFrameworkContractType(form.value.contractType)
|
||
)
|
||
if (!confirmedZero) return
|
||
|
||
submitLoading.value = true
|
||
try {
|
||
rebuildContractName()
|
||
const attachmentJson = serializeContractAttachments(attachmentList.value)
|
||
const data: any = { ...form.value }
|
||
|
||
if (attachmentJson) {
|
||
data.attachmentFiles = attachmentJson
|
||
}
|
||
|
||
omitEmptyExceptPartyFields(data)
|
||
|
||
if (data.contactsName) {
|
||
data.contactName = data.contactsName
|
||
}
|
||
if (data.contactsPhone) {
|
||
data.contactMobile = data.contactsPhone
|
||
}
|
||
|
||
delete data.customerName
|
||
delete data.opportunityName
|
||
delete data.quoteNo
|
||
delete data.contactsName
|
||
delete data.contactsPhone
|
||
delete data.ownerUserName
|
||
delete data.templateContent
|
||
|
||
if (data.products && Array.isArray(data.products) && data.products.length > 0) {
|
||
data.products = buildProductsPayload()
|
||
} else {
|
||
delete data.products
|
||
}
|
||
|
||
let contractId: string | number
|
||
if (form.value.id) {
|
||
await updateContract(data)
|
||
contractId = form.value.id
|
||
$modal.msgSuccess('保存成功')
|
||
} else {
|
||
const res = await addContract(data)
|
||
contractId = res.data
|
||
if (contractId == null || contractId === '') {
|
||
$modal.msgError('保存成功但未返回合同 ID,请从列表重新提交审批')
|
||
emit('success')
|
||
return
|
||
}
|
||
form.value.id = contractId as any
|
||
$modal.msgSuccess('保存成功')
|
||
}
|
||
|
||
approvalDialogRef.value?.open(contractId, 'submit')
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 审批成功回调 */
|
||
const handleApprovalSuccess = () => {
|
||
handleClose()
|
||
emit('success')
|
||
}
|
||
|
||
/** 关闭对话框 */
|
||
const handleClose = () => {
|
||
dialogVisible.value = false
|
||
}
|
||
|
||
/** 加载供方信息列表 */
|
||
const loadSupplierList = async () => {
|
||
try {
|
||
const res = await listAllSupplierInfo({ status: '0' })
|
||
supplierList.value = res.data || []
|
||
return res.data || []
|
||
} catch (error) {
|
||
console.error('加载供方信息列表失败:', error)
|
||
return []
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.contract-form-dialog {
|
||
:deep(.el-dialog__wrapper) {
|
||
padding: 0 !important;
|
||
top: 20px !important;
|
||
height: calc(100vh - 40px) !important;
|
||
}
|
||
|
||
:deep(.el-dialog) {
|
||
backdrop-filter: none;
|
||
height: 100% !important;
|
||
max-height: calc(100vh - 40px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.dialog-footer) {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 16px;
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-dialog__header),
|
||
:deep(.el-dialog__footer) {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding-top: 8px;
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
max-height: none;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
}
|
||
|
||
.contract-form-body {
|
||
position: relative;
|
||
min-height: 280px;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.contract-basic-info {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 12px 14px;
|
||
margin-bottom: 20px;
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.el-input-number) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1100px) {
|
||
.contract-basic-info {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 560px) {
|
||
.contract-basic-info {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.parties-section {
|
||
margin: 8px 0 20px;
|
||
}
|
||
|
||
.parties-section-toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.parties-row {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
.party-panel {
|
||
flex: 1 1 0;
|
||
min-width: 0;
|
||
padding: 16px;
|
||
border: 1px solid var(--border, #d9e8e2);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
|
||
&.is-collapsed {
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.party-panel__header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
.party-panel.is-collapsed & {
|
||
margin-bottom: 8px;
|
||
}
|
||
}
|
||
|
||
.party-panel__title {
|
||
margin-bottom: 0;
|
||
padding-left: 10px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--section-bar-title-color, var(--primary-dark, #059669));
|
||
line-height: 1.5;
|
||
border-left: 3px solid var(--section-bar-border-color, var(--primary, #10b981));
|
||
}
|
||
|
||
.party-panel__summary {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
padding: 2px 0 2px 10px;
|
||
}
|
||
|
||
.party-panel__summary-label {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #64748b);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.party-panel__summary-value {
|
||
min-width: 0;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--mint-text, #0f172a);
|
||
line-height: 1.5;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
|
||
&.is-empty {
|
||
font-weight: 400;
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
}
|
||
}
|
||
|
||
.party-flow-bridge {
|
||
flex: 0 0 88px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.party-flow-bridge__label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--primary-dark, #168f45);
|
||
letter-spacing: 0.06em;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
background: #e6f8ed;
|
||
border: 1px solid #a8e0b8;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.party-flow-bridge__arrow {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 50%;
|
||
color: var(--primary-dark, #168f45);
|
||
background: linear-gradient(145deg, #e6f8ed 0%, #d4f0de 100%);
|
||
border: 1px solid #a8e0b8;
|
||
box-shadow: 0 2px 8px rgba(28, 168, 81, 0.14);
|
||
|
||
svg {
|
||
width: 22px;
|
||
height: 22px;
|
||
display: 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);
|
||
}
|
||
}
|
||
|
||
.total-section {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background-color: #f5f7fa;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.total-item {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
font-size: 16px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.total-label {
|
||
font-weight: bold;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.total-value {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.text-muted {
|
||
color: #1e293b;
|
||
}
|
||
|
||
:deep(.money-field-input.el-input-number .el-input__inner),
|
||
:deep(.money-field-input.el-input-number .el-input__wrapper input) {
|
||
color: #dc2626 !important;
|
||
-webkit-text-fill-color: #dc2626 !important;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 统一表格内输入框和下拉框高度 */
|
||
:deep(.el-table .el-input__wrapper) {
|
||
height: 32px !important;
|
||
min-height: 32px !important;
|
||
}
|
||
|
||
:deep(.el-table .el-select__wrapper) {
|
||
height: 32px !important;
|
||
min-height: 32px !important;
|
||
}
|
||
|
||
:deep(.el-table .el-input--small .el-input__wrapper) {
|
||
height: 32px !important;
|
||
min-height: 32px !important;
|
||
}
|
||
|
||
:deep(.el-table .el-select--small .el-select__wrapper) {
|
||
height: 32px !important;
|
||
min-height: 32px !important;
|
||
}
|
||
|
||
/* 价格输入框红色文字 */
|
||
.price-input :deep(.el-input__inner) {
|
||
color: #e34d59;
|
||
}
|
||
|
||
/* 去除输入框后置按钮的边框和背景 */
|
||
:deep(.el-input-group__append) {
|
||
background: transparent !important;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
:deep(.el-input-group__append .el-button) {
|
||
border: none !important;
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
padding: 0 15px !important;
|
||
}
|
||
|
||
:deep(.el-input-group__append .el-button:hover) {
|
||
background: transparent !important;
|
||
border: none !important;
|
||
}
|
||
|
||
:deep(.el-input-group__append .el-button:focus) {
|
||
background: transparent !important;
|
||
border: none !important;
|
||
}
|
||
|
||
/* 框架合同产品明细:默认文案 + 可切换填数字 */
|
||
.framework-pricing-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.framework-pricing-cell--money {
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.framework-pricing-cell__label {
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.framework-pricing-cell__action {
|
||
padding: 0 !important;
|
||
height: auto !important;
|
||
font-size: 12px !important;
|
||
}
|
||
|
||
/* 框架合同调价函:表格内紧凑附件上传 */
|
||
.fw-price-tier-table :deep(.attachment-upload--compact:not(.attachment-upload--readonly)) {
|
||
min-width: 0;
|
||
|
||
.attachment-upload {
|
||
width: auto;
|
||
}
|
||
|
||
.attachment-list {
|
||
flex-wrap: wrap;
|
||
align-items: flex-start;
|
||
}
|
||
}
|
||
|
||
/* 框架合同:增加「调价函」行内按钮可点区域宽度 */
|
||
.fw-tier-letter-add-btn.el-button--primary.is-link {
|
||
min-width: 108px;
|
||
padding-left: 12px;
|
||
padding-right: 12px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.mb-3 {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.mb-4 {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.ml-2 {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.line-meta-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
.line-meta-cell__control {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.line-meta-cell__empty {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.line-meta-cell__btn {
|
||
flex-shrink: 0;
|
||
padding: 4px;
|
||
}
|
||
|
||
.customer-select-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
width: 100%;
|
||
}
|
||
|
||
.customer-select-row__select {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.customer-select-row__search {
|
||
flex-shrink: 0;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.customer-option-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.customer-option-row__meta {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #8492a6);
|
||
}
|
||
</style>
|