3178 lines
115 KiB
Vue
3178 lines
115 KiB
Vue
<template>
|
||
<basic-container
|
||
:class="['contract-manage-page', { 'contract-manage-page--form': isFormPage || isDetailPage }]"
|
||
>
|
||
<template v-if="isListPage">
|
||
<avue-crud
|
||
ref="crud"
|
||
v-model:page="page"
|
||
:data="data"
|
||
:option="tableOption"
|
||
:permission="permissionList"
|
||
:table-loading="loading"
|
||
@search-change="searchChange"
|
||
@search-reset="searchReset"
|
||
@selection-change="selectionChange"
|
||
@current-change="currentChange"
|
||
@size-change="sizeChange"
|
||
@refresh-change="refreshChange"
|
||
@search-icon-change="searchExpanded = $event"
|
||
@on-load="onLoad"
|
||
>
|
||
<template #menu-left>
|
||
<el-button v-if="canCreate" type="primary" icon="el-icon-plus" @click="openForm('add')">
|
||
新增
|
||
</el-button>
|
||
<el-button
|
||
v-if="config.exportUrl && hasPermission(`${config.permission}_export`)"
|
||
type="primary"
|
||
icon="el-icon-download"
|
||
plain
|
||
@click="handleExport"
|
||
>
|
||
批量导出
|
||
</el-button>
|
||
</template>
|
||
|
||
<template #header>
|
||
<div class="contract-manage-page__expiry-search">
|
||
<el-check-tag
|
||
v-for="item in expiryTagOptions"
|
||
:key="item.value"
|
||
:checked="contractExpiryScope === item.value"
|
||
@change="changeExpiryScope(item.value)"
|
||
>
|
||
{{ item.label }}({{ contractExpiryStats[item.statKey] || 0 }})
|
||
</el-check-tag>
|
||
</div>
|
||
</template>
|
||
|
||
<template #contractName="{ row }">
|
||
<el-link v-if="row.contractName" type="primary" class="contract-name-cell" @click.stop="openDetail(row)">
|
||
{{ row.contractName }}
|
||
</el-link>
|
||
<span v-else>-</span>
|
||
</template>
|
||
|
||
<template #contractStage="{ row }">
|
||
<el-tag :type="stageTagType(row.contractStage)" class="status-text">
|
||
{{ displayStatus(row, 'contractStage') }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #approvalStatus="{ row }">
|
||
<el-tag :type="statusTagType(row.approvalStatus)" class="status-text">
|
||
{{ displayStatus(row, 'approvalStatus') }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #archiveStatus="{ row }">
|
||
<el-tag :type="archiveStatusTagType(row.archiveStatus)" class="status-text">
|
||
{{ row.archiveStatus || '未归档' }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #menu="{ row }">
|
||
<el-link
|
||
v-if="hasPermission(`${config.permission}_view`)"
|
||
type="primary"
|
||
@click="openDetail(row)"
|
||
>
|
||
查看
|
||
</el-link>
|
||
<el-link v-if="canEdit(row)" type="primary" @click="openForm('edit', row)">
|
||
{{ editLabel(row) }}
|
||
</el-link>
|
||
<el-link v-if="canCopy(row)" type="primary" @click="handleCopy(row)">复制</el-link>
|
||
<el-link
|
||
v-for="operation in customOperations(row)"
|
||
:key="`${operation.action}-${operation.label}`"
|
||
:type="operation.type || 'primary'"
|
||
@click="handleOperation(operation, row)"
|
||
>
|
||
{{ operation.label }}
|
||
</el-link>
|
||
<el-link v-if="canDelete(row)" type="danger" @click="removeRow(row)">删除</el-link>
|
||
</template>
|
||
</avue-crud>
|
||
|
||
<empty-pagination
|
||
:page="page"
|
||
@size-change="sizeChange"
|
||
@current-change="currentChange"
|
||
@load="onLoad(page, query)"
|
||
/>
|
||
</template>
|
||
|
||
<template v-else-if="isDetailPage">
|
||
<div class="archive-page-form__title">{{ detailPageTitle }}</div>
|
||
<div v-loading="detailLoading" class="contract-manage-form contract-manage-detail">
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">基本信息</div>
|
||
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
|
||
<el-descriptions-item label="签约类型">{{
|
||
detailValue('signType')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="合同编号">{{
|
||
detailValue('contractNo')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="合同名称">{{
|
||
detailValue('contractName')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="合同类型">{{
|
||
detailValue('contractCategory')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="项目">{{
|
||
detailValue('projectName')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="甲方">{{ detailValue('partyA') }}</el-descriptions-item>
|
||
<el-descriptions-item label="乙方">{{ detailValue('partyB') }}</el-descriptions-item>
|
||
<el-descriptions-item label="合同期限">{{ detailContractPeriod }}</el-descriptions-item>
|
||
<el-descriptions-item label="所属组织">{{
|
||
detailValue('organizationName')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="经办人">{{
|
||
detailValue('handlerUserName')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="归档状态">{{
|
||
detailRow.archiveStatus || '未归档'
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="签订日期">{{
|
||
detailValue('signDate')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="合同格式">{{
|
||
detailValue('contractFormat')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="是否需要加盖法人章">{{
|
||
detailLegalSealText
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="一式">{{
|
||
detailUnitValue('copyCount', '份')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="结算币种">{{
|
||
detailDictionaryValue('settlementCurrency', settlementCurrencyOptions)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="结算方式">{{
|
||
detailDictionaryValue('settlementMode', settlementModeOptions)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="开票周期">{{
|
||
detailUnitValue('invoiceCycle', '天')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="回款账期">{{
|
||
detailUnitValue('paymentDays', '天')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="合同金额">{{
|
||
detailValue('contractAmount')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="是否范本">{{ detailTemplateText }}</el-descriptions-item>
|
||
<el-descriptions-item label="原件合同编号">{{
|
||
detailValue('originalContractNo')
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="是否电子章">
|
||
{{ detailElectronicSealText }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="3">{{
|
||
detailValue('remark')
|
||
}}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</section>
|
||
|
||
<contract-attachment-section
|
||
title="合同文件"
|
||
description
|
||
attachment-type
|
||
readonly
|
||
:rows="detailContractFileRows"
|
||
:preview="previewAttachment"
|
||
/>
|
||
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">计费信息</div>
|
||
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
|
||
<el-descriptions-item label="费用生成模式">{{
|
||
detailFeeGenerationMode
|
||
}}</el-descriptions-item>
|
||
</el-descriptions>
|
||
<el-table :data="detailBillingPlanRows" border>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="planName" label="方案名称" min-width="220" align="center" />
|
||
<el-table-column label="运输方式" min-width="180" align="center">
|
||
<template #default="{ row }">{{
|
||
row.transportModeLabel || row.transportMode || '-'
|
||
}}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<el-link type="primary" @click="openDetailBillingPlan(row, $index)"
|
||
>查看详情</el-link
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">结算单规则</div>
|
||
<el-tabs v-model="detailSettlementConfigTab">
|
||
<el-tab-pane label="预结算配置" name="pre" />
|
||
<el-tab-pane label="正式结算配置" name="formal" />
|
||
</el-tabs>
|
||
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
|
||
<el-descriptions-item label="自动生成结算单">{{
|
||
Number(detailSettlementRule.autoGenerate) === 1 ? '开启' : '关闭'
|
||
}}</el-descriptions-item>
|
||
<template v-if="Number(detailSettlementRule.autoGenerate) === 1">
|
||
<el-descriptions-item label="账单起始日期">{{
|
||
displayValue(detailSettlementRule.billStartDate)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item label="结算类型">{{
|
||
displayValue(detailSettlementRule.settlementType)
|
||
}}</el-descriptions-item>
|
||
<el-descriptions-item
|
||
v-if="detailSettlementRule.settlementType === '月结'"
|
||
label="结算周期"
|
||
>{{ displayValue(detailSettlementRule.billCycleType) }}</el-descriptions-item
|
||
>
|
||
<el-descriptions-item
|
||
v-if="
|
||
detailSettlementRule.settlementType === '月结' &&
|
||
detailSettlementRule.billCycleType === '固定截单日'
|
||
"
|
||
label="账单截单日"
|
||
>{{
|
||
detailObjectUnitValue(detailSettlementRule, 'billCutoffDay', '日')
|
||
}}</el-descriptions-item
|
||
>
|
||
<el-descriptions-item
|
||
v-if="detailSettlementRule.settlementType === '固定天数周期结算'"
|
||
label="周期天数"
|
||
>{{
|
||
detailObjectUnitValue(detailSettlementRule, 'cycleDays', '天')
|
||
}}</el-descriptions-item
|
||
>
|
||
</template>
|
||
</el-descriptions>
|
||
<el-table
|
||
v-if="
|
||
Number(detailSettlementRule.autoGenerate) === 1 &&
|
||
detailSettlementRule.settlementType === '月结' &&
|
||
detailSettlementRule.billCycleType === '自定义多周期'
|
||
"
|
||
:data="detailSettlementRule.customPeriods || []"
|
||
border
|
||
class="contract-manage-form__custom-periods"
|
||
>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column label="运单区间-开始日" min-width="180" align="center">
|
||
<template #default="{ row }">{{ row.startDay || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="运单区间-结束日" min-width="180" align="center">
|
||
<template #default="{ row }">{{ row.endDay || '-' }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">
|
||
付款比例设置
|
||
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
|
||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"
|
||
><el-icon-question-filled
|
||
/></el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
<el-table :data="detailPaymentRatioRows" border>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" align="center" />
|
||
<el-table-column label="付款比例上限(%)" min-width="220" align="center">
|
||
<template #default="{ row }">{{
|
||
detailObjectUnitValue(row, 'ratioLimit', '%')
|
||
}}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="220" />
|
||
</el-table>
|
||
</section>
|
||
|
||
<contract-attachment-section
|
||
title="其它附件"
|
||
description
|
||
readonly
|
||
:rows="detailAttachmentRows"
|
||
:preview="previewAttachment"
|
||
/>
|
||
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">变更记录</div>
|
||
<el-table :data="detailChangeRecordRows" border>
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column
|
||
prop="changeDate"
|
||
label="变更日期"
|
||
min-width="150"
|
||
align="center"
|
||
sortable
|
||
/>
|
||
<el-table-column prop="handlerUserName" label="经办人" min-width="140" align="center" />
|
||
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
|
||
<el-table-column label="变更内容" min-width="420">
|
||
<template #default="{ row }">
|
||
<el-tooltip placement="top" :show-after="200">
|
||
<template #content>
|
||
<div class="contract-change-record-content-tooltip">
|
||
{{ formatContractChangeContent(row) }}
|
||
</div>
|
||
</template>
|
||
<span class="contract-change-record-content-cell">{{
|
||
formatContractChangeContent(row)
|
||
}}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="changeReason"
|
||
label="变更原因"
|
||
min-width="240"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="statusName" label="状态" min-width="140" align="center" />
|
||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||
<template #default="{ row }"
|
||
><el-link type="primary" @click="openDetailChangeRecord(row)">详情</el-link></template
|
||
>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
</div>
|
||
<div class="contract-manage-page__footer">
|
||
<el-button @click="closeDetail">关闭</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<div class="archive-page-form__title">{{ formPageTitle }}</div>
|
||
|
||
<el-form
|
||
ref="contractForm"
|
||
:model="form"
|
||
:rules="formRules"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="contract-manage-form"
|
||
:class="{ 'contract-manage-form--attachment-upload': isAttachmentUploadMode }"
|
||
>
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">基本信息</div>
|
||
<div class="contract-manage-form__grid">
|
||
<el-form-item label="签约类型" prop="signType">
|
||
<el-select v-model="form.signType" placeholder="请选择签约类型">
|
||
<el-option
|
||
v-for="item in signTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="合同编号">
|
||
<el-input
|
||
v-model="form.contractNo"
|
||
:disabled="formMode === 'edit'"
|
||
placeholder="不填写,则系统自动生成"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同名称" prop="contractName">
|
||
<el-input
|
||
v-model="form.contractName"
|
||
maxlength="100"
|
||
show-word-limit
|
||
placeholder="请输入合同名称"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同类型" prop="contractCategory">
|
||
<el-select v-model="form.contractCategory" placeholder="请选择合同类型">
|
||
<el-option
|
||
v-for="item in contractCategoryOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目" prop="projectName">
|
||
<el-select
|
||
v-model="selectedProjectId"
|
||
placeholder="请选择"
|
||
filterable
|
||
clearable
|
||
:loading="projectLoading"
|
||
@visible-change="visible => visible && loadProjectOptions()"
|
||
@change="handleProjectChange"
|
||
>
|
||
<el-option
|
||
v-for="item in projectOptions"
|
||
:key="item.id"
|
||
:label="item.projectName"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="所属组织" prop="organizationName">
|
||
<el-cascader
|
||
v-model="organizationCascaderValue"
|
||
:options="organizationOptions"
|
||
:props="organizationCascaderProps"
|
||
placeholder="请选择"
|
||
clearable
|
||
filterable
|
||
@visible-change="visible => visible && loadOrganizationOptions()"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="甲方" prop="partyA">
|
||
<el-select
|
||
v-model="form.partyA"
|
||
placeholder="请选择甲方"
|
||
filterable
|
||
clearable
|
||
:loading="contractPartyLoading"
|
||
@visible-change="visible => visible && loadContractPartyOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in contractPartyAOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="乙方" prop="partyB">
|
||
<el-select
|
||
v-model="form.partyB"
|
||
placeholder="请选择乙方"
|
||
filterable
|
||
clearable
|
||
:loading="contractPartyLoading"
|
||
@visible-change="visible => visible && loadContractPartyOptions()"
|
||
>
|
||
<el-option
|
||
v-for="item in contractPartyBOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="签订日期" prop="signDate">
|
||
<el-date-picker
|
||
v-model="form.signDate"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择签订日期"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="合同期限" prop="startDate">
|
||
<div class="contract-manage-form__date-range">
|
||
<el-date-picker
|
||
v-model="form.startDate"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="YYYY-MM-DD"
|
||
/>
|
||
<span>至</span>
|
||
<el-date-picker
|
||
v-model="form.endDate"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="YYYY-MM-DD"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="回款账期">
|
||
<el-input
|
||
v-model="form.paymentDays"
|
||
placeholder="请输入"
|
||
@input="value => integerInput('paymentDays', value)"
|
||
><template #suffix>天</template></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="合同金额">
|
||
<el-input-number
|
||
v-model="form.contractAmount"
|
||
:min="0"
|
||
:precision="2"
|
||
:controls="false"
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="是否范本">
|
||
<el-select v-model="form.templateFlag" placeholder="请选择">
|
||
<el-option label="否" :value="0" />
|
||
<el-option label="是" :value="1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="原件合同编号">
|
||
<el-input v-model="form.originalContractNo" maxlength="100" placeholder="请输入" />
|
||
</el-form-item>
|
||
<el-form-item label="是否电子章">
|
||
<el-select v-model="form.electronicSealFlag" placeholder="请选择">
|
||
<el-option label="否" :value="0" />
|
||
<el-option label="是" :value="1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="开票周期">
|
||
<el-input
|
||
v-model="form.invoiceCycle"
|
||
placeholder="请输入"
|
||
@input="value => positiveIntegerInput('invoiceCycle', value)"
|
||
><template #suffix>天</template></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="结算币种" prop="settlementCurrency">
|
||
<el-select v-model="form.settlementCurrency" placeholder="请选择结算币种" clearable>
|
||
<el-option
|
||
v-for="item in settlementCurrencyOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="合同格式" prop="contractFormat">
|
||
<el-select v-model="form.contractFormat" placeholder="请选择合同格式">
|
||
<el-option
|
||
v-for="item in contractFormatOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="一式">
|
||
<el-input
|
||
v-model="form.copyCount"
|
||
placeholder="请输入"
|
||
@input="value => integerInput('copyCount', value)"
|
||
><template #suffix>份</template></el-input
|
||
>
|
||
</el-form-item>
|
||
<el-form-item label="是否需要加盖法人章" prop="legalSealFlag">
|
||
<el-select v-model="form.legalSealFlag" placeholder="请选择">
|
||
<el-option label="否" :value="0" />
|
||
<el-option label="是" :value="1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="结算方式" prop="settlementMode">
|
||
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable>
|
||
<el-option
|
||
v-for="item in settlementModeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="经办人">
|
||
<el-input v-model="form.handlerUserName" disabled />
|
||
</el-form-item>
|
||
</div>
|
||
<el-form-item label="备注" prop="remark" class="contract-manage-form__remark">
|
||
<el-input
|
||
v-model="form.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="请输入备注"
|
||
/>
|
||
</el-form-item>
|
||
</section>
|
||
|
||
<contract-attachment-section
|
||
title="合同文件"
|
||
description
|
||
attachment-type
|
||
use-upload-dialog
|
||
:lock-approved="isAttachmentUploadMode || hasApprovedContractFiles"
|
||
:mark-approved-on-upload="isAttachmentUploadMode"
|
||
:rows="contractFileRows"
|
||
:preview="previewAttachment"
|
||
@update:rows="contractFileRows = $event"
|
||
/>
|
||
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">计费信息</div>
|
||
<div class="contract-manage-form__billing-head">
|
||
<div>
|
||
<span>费用生成模式</span>
|
||
<el-radio-group v-model="form.feeGenerationMode">
|
||
<el-radio label="system">系统生成</el-radio>
|
||
<el-tooltip content="运单完成后,系统根据计费规则自动生成应收应付" placement="top">
|
||
<el-icon class="contract-manage-form__fee-mode-tip"><QuestionFilled /></el-icon>
|
||
</el-tooltip>
|
||
<el-radio label="manual">账单导入生成</el-radio>
|
||
</el-radio-group>
|
||
</div>
|
||
<el-button type="primary" @click="openBillingPlan()">添加</el-button>
|
||
</div>
|
||
<el-table :data="billingPlanRows" border>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="planName" label="方案名称" min-width="220" align="center" />
|
||
<el-table-column label="运输方式" min-width="180" align="center">
|
||
<template #default="{ row }">{{
|
||
row.transportModeLabel || row.transportMode || '-'
|
||
}}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<el-link type="primary" @click="openBillingPlan(row, $index)">编辑</el-link>
|
||
<el-link type="danger" @click="removeBillingPlan($index)">删除</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="dialog-section-title">结算单规则</div>
|
||
<el-tabs v-model="settlementConfigTab">
|
||
<el-tab-pane label="预结算配置" name="pre" />
|
||
<el-tab-pane label="正式结算配置" name="formal" />
|
||
</el-tabs>
|
||
<div class="contract-manage-form__settlement-switch">
|
||
<span>自动生成结算单</span>
|
||
<el-radio-group v-model="settlementRuleForm.autoGenerate">
|
||
<el-radio :label="1">开启</el-radio>
|
||
<el-tooltip
|
||
content="开启时,系统根据配置规则归集运单,定时生成结算单"
|
||
placement="top"
|
||
>
|
||
<el-icon class="settlement-switch-tip"><QuestionFilled /></el-icon>
|
||
</el-tooltip>
|
||
<el-radio :label="0">关闭</el-radio>
|
||
</el-radio-group>
|
||
</div>
|
||
<div
|
||
v-if="settlementRuleEnabled"
|
||
class="contract-manage-form__grid contract-manage-form__settlement-grid"
|
||
>
|
||
<el-form-item label="账单起始日期" required>
|
||
<el-date-picker
|
||
v-model="settlementRuleForm.billStartDate"
|
||
type="date"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="请选择账单起始日期"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="结算类型" required>
|
||
<el-select
|
||
v-model="settlementRuleForm.settlementType"
|
||
placeholder="请选择结算类型"
|
||
@change="handleSettlementTypeChange"
|
||
>
|
||
<el-option
|
||
v-for="item in settlementTypeOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="showBillCycleType" label="结算周期" required>
|
||
<el-select
|
||
v-model="settlementRuleForm.billCycleType"
|
||
placeholder="请选择结算周期"
|
||
@change="handleBillCycleTypeChange"
|
||
>
|
||
<el-option
|
||
v-for="item in billCycleTypeOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="showBillCutoffDay" label="账单截单日" required>
|
||
<el-select v-model="settlementRuleForm.billCutoffDay" placeholder="请选择账单截单日">
|
||
<el-option
|
||
v-for="item in billCutoffDayOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="showCycleDays" label="周期天数" required>
|
||
<el-select v-model="settlementRuleForm.cycleDays" placeholder="请选择周期天数">
|
||
<el-option
|
||
v-for="item in cycleDayOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
<div
|
||
v-if="settlementRuleEnabled && showCustomPeriods"
|
||
class="contract-manage-form__custom-periods"
|
||
>
|
||
<el-table :data="settlementRuleForm.customPeriods" border>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column label="运单区间-开始日" min-width="180" align="center">
|
||
<template #default="{ row, $index }">
|
||
<el-select
|
||
:model-value="row.startDay"
|
||
placeholder="请选择"
|
||
clearable
|
||
@update:model-value="value => handleCustomPeriodStartDayChange($index, value)"
|
||
>
|
||
<el-option
|
||
v-for="item in billCutoffDayOptions"
|
||
:key="`start-${item.value}`"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="运单区间-结束日" min-width="200" align="center">
|
||
<template #default="{ row, $index }">
|
||
<el-select
|
||
:model-value="row.endDay"
|
||
placeholder="请选择"
|
||
clearable
|
||
@update:model-value="value => handleCustomPeriodEndDayChange($index, value)"
|
||
>
|
||
<el-option
|
||
v-for="item in customPeriodEndDayOptions(row)"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="100" align="center">
|
||
<template #default="{ $index }">
|
||
<el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow"
|
||
>添加</el-link
|
||
>
|
||
<el-link v-else type="danger" @click="removeCustomPeriodRow($index)"
|
||
>删除</el-link
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-link type="primary" @click="addCustomPeriodRow">添加</el-link>
|
||
</template>
|
||
</el-table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="contract-manage-form__section contract-manage-form__section--panel">
|
||
<div class="contract-manage-form__section-head">
|
||
<div class="dialog-section-title">
|
||
付款比例设置
|
||
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
|
||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
<el-button type="primary" @click="addPaymentRatio">添加</el-button>
|
||
</div>
|
||
<el-table :data="paymentRatioRows" border>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" align="center" />
|
||
<el-table-column label="付款比例上限(%)" min-width="220" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.ratioLimit"
|
||
placeholder="请输入"
|
||
@input="value => ratioInput(row, value)"
|
||
>
|
||
<template #append>%</template>
|
||
</el-input>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="220">
|
||
<template #default="{ row }">
|
||
<el-input v-model="row.remark" maxlength="200" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="100" align="center">
|
||
<template #default="{ $index }">
|
||
<el-link type="danger" @click="paymentRatioRows.splice($index, 1)">删除</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
|
||
<contract-attachment-section
|
||
title="其它附件"
|
||
description
|
||
attachment-type
|
||
use-upload-dialog
|
||
attachment-location="其它附件"
|
||
:readonly="isAttachmentUploadMode"
|
||
:rows="attachmentRows"
|
||
:preview="previewAttachment"
|
||
@update:rows="attachmentRows = $event"
|
||
/>
|
||
|
||
<section
|
||
v-if="formMode === 'edit'"
|
||
class="contract-manage-form__section contract-manage-form__section--panel"
|
||
>
|
||
<div class="dialog-section-title">变更记录</div>
|
||
<el-table :data="changeRecordRows" border>
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column
|
||
prop="changeDate"
|
||
label="变更日期"
|
||
min-width="150"
|
||
align="center"
|
||
sortable
|
||
/>
|
||
<el-table-column prop="handlerUserName" label="经办人" min-width="140" align="center" />
|
||
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
|
||
<el-table-column label="变更内容" min-width="420">
|
||
<template #default="{ row }">
|
||
<el-tooltip placement="top" :show-after="200">
|
||
<template #content>
|
||
<div class="contract-change-record-content-tooltip">
|
||
{{ formatContractChangeContent(row) }}
|
||
</div>
|
||
</template>
|
||
<span class="contract-change-record-content-cell">{{
|
||
formatContractChangeContent(row)
|
||
}}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="changeReason"
|
||
label="变更原因"
|
||
min-width="240"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="statusName" label="状态" min-width="140" align="center" />
|
||
<el-table-column label="操作" width="150" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="openDetailChangeRecord(row)">详情</el-link>
|
||
<el-link type="primary" @click="openFlow(row)">流程</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</section>
|
||
</el-form>
|
||
|
||
<div class="contract-manage-page__footer">
|
||
<el-button @click="closeForm">关闭</el-button>
|
||
<template v-if="isAttachmentUploadMode">
|
||
<el-button
|
||
type="primary"
|
||
:loading="submitAction === 'attachment'"
|
||
:disabled="Boolean(submitAction)"
|
||
@click="submitAttachmentUpload"
|
||
>
|
||
保存
|
||
</el-button>
|
||
</template>
|
||
<template v-else-if="formMode === 'add' || isDraftEdit">
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
:loading="submitAction === 'draft'"
|
||
:disabled="Boolean(submitAction)"
|
||
@click="submitCreate('draft')"
|
||
>
|
||
保存
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
:loading="submitAction === 'temporary'"
|
||
:disabled="Boolean(submitAction)"
|
||
@click="submitCreate('temporary')"
|
||
>
|
||
提交临时合同
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="submitAction === 'formal'"
|
||
:disabled="Boolean(submitAction)"
|
||
@click="submitCreate('formal')"
|
||
>
|
||
提交正式合同
|
||
</el-button>
|
||
</template>
|
||
<el-button v-else type="primary" :loading="submitAction === 'edit'" @click="submitEdit">
|
||
提交
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<billing-plan-editor
|
||
ref="billingPlanEditor"
|
||
v-model="billingPlanBox"
|
||
:value="billingPlanForm"
|
||
:index="billingPlanIndex"
|
||
@save="saveBillingPlan"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="attachmentDocumentPreviewVisible"
|
||
:title="attachmentPreviewFile.name || '附件预览'"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="90%"
|
||
top="4vh"
|
||
>
|
||
<pdf-preview
|
||
v-if="
|
||
attachmentDocumentPreviewVisible &&
|
||
attachmentPreviewFile.url &&
|
||
attachmentPreviewFile.isPdf
|
||
"
|
||
:source="attachmentPreviewFile.url"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
<open-file-viewer
|
||
v-else-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
|
||
:file="attachmentPreviewFile.url"
|
||
:file-name="attachmentPreviewFile.name"
|
||
:mime-type="attachmentPreviewFile.mimeType"
|
||
width="100%"
|
||
height="72vh"
|
||
fit="contain"
|
||
theme="auto"
|
||
locale="zh-CN"
|
||
:toolbar="attachmentViewerToolbar"
|
||
:plugins="attachmentViewerPlugins"
|
||
@unsupported="handleAttachmentPreviewUnsupported"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
</el-dialog>
|
||
|
||
<el-image-viewer
|
||
v-if="attachmentImagePreviewVisible"
|
||
:url-list="attachmentImagePreviewUrls"
|
||
:initial-index="attachmentImagePreviewIndex"
|
||
@close="attachmentImagePreviewVisible = false"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="detailChangeRecordVisible"
|
||
title="变更记录详情"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="1100px"
|
||
top="10px"
|
||
class="contract-change-record-detail-dialog"
|
||
>
|
||
<div v-if="detailChangeRecord" class="contract-change-record-detail-meta">
|
||
<span>经办人:{{ detailChangeRecord.handlerUserName || '-' }}</span>
|
||
<span>变更类型:{{ detailChangeRecord.changeType || '-' }}</span>
|
||
<span>状态:{{ detailChangeRecord.statusName || detailChangeRecord.status || '-' }}</span>
|
||
</div>
|
||
<el-table :data="detailChangeRecordDetailRows" border :show-overflow-tooltip="false">
|
||
<el-table-column prop="field" label="变更字段" min-width="180" />
|
||
<el-table-column
|
||
prop="before"
|
||
label="变更前"
|
||
min-width="360"
|
||
class-name="contract-change-record-detail-value"
|
||
/>
|
||
<el-table-column
|
||
prop="after"
|
||
label="变更后"
|
||
min-width="500"
|
||
class-name="contract-change-record-detail-value"
|
||
/>
|
||
</el-table>
|
||
<el-empty
|
||
v-if="!detailChangeRecordDetailRows.length"
|
||
description="暂无变更内容"
|
||
:image-size="60"
|
||
/>
|
||
<template #footer>
|
||
<el-button type="primary" @click="detailChangeRecordVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<billing-plan-editor
|
||
v-model="detailBillingPlanBox"
|
||
:value="detailBillingPlanForm"
|
||
:index="detailBillingPlanIndex"
|
||
readonly
|
||
/>
|
||
|
||
<flow-design
|
||
v-if="website.design.designMode"
|
||
is-dialog
|
||
v-model:is-display="flowBox"
|
||
:process-instance-id="processInstanceId"
|
||
/>
|
||
<el-dialog v-else v-model="flowBox" title="流程图" append-to-body fullscreen>
|
||
<iframe class="contract-manage-page__flow" :src="flowUrl" />
|
||
<template #footer><el-button @click="flowBox = false">关闭</el-button></template>
|
||
</el-dialog>
|
||
</basic-container>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters } from 'vuex';
|
||
import NProgress from 'nprogress';
|
||
import 'nprogress/nprogress.css';
|
||
import { exportBlob } from '@/api/common';
|
||
import * as api from '@/api/business/contract-manage';
|
||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
|
||
import { getDeptTree } from '@/api/system/dept';
|
||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||
import { config, option } from '@/option/business/contract-manage';
|
||
import { getToken } from '@/utils/auth';
|
||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||
import ContractAttachmentSection, {
|
||
attachmentName as sharedAttachmentName,
|
||
attachmentUrl as sharedAttachmentUrl,
|
||
normalizeContractFileRows,
|
||
} from './components/contract-attachment-section.vue';
|
||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||
import { ElImageViewer, ElCascader } from 'element-plus';
|
||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
|
||
import '@open-file-viewer/core/style.css';
|
||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||
|
||
const clone = value => JSON.parse(JSON.stringify(value));
|
||
const normalizeOptionalInteger = (value, emptyValue = null) => {
|
||
if (value === undefined || value === null || value === '' || Number(value) < 0) {
|
||
return emptyValue;
|
||
}
|
||
const number = Number(value);
|
||
return Number.isFinite(number) ? Math.trunc(number) : emptyValue;
|
||
};
|
||
const normalizeOptionalAmount = (value, emptyValue = null) => {
|
||
if (value === undefined || value === null || value === '') return emptyValue;
|
||
const number = Number(value);
|
||
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : emptyValue;
|
||
};
|
||
const normalizeProjectValue = value => {
|
||
if (value === undefined || value === null || value === '' || String(value) === '-1') return '';
|
||
return value;
|
||
};
|
||
const defaultSettlementRule = () => ({
|
||
autoGenerate: '',
|
||
billStartDate: '',
|
||
settlementType: '',
|
||
billCycleType: '',
|
||
billCutoffDay: '',
|
||
cycleDays: '',
|
||
customPeriods: [],
|
||
});
|
||
const normalizeCustomPeriods = (periods = []) =>
|
||
(periods || []).map(item => ({
|
||
startDay: Number(item?.startDay) > 0 ? Number(item.startDay) : '',
|
||
endDay: Number(item?.endDay) > 0 ? Number(item.endDay) : '',
|
||
}));
|
||
const defaultForm = () => ({
|
||
contractCategory: '',
|
||
contractNo: '',
|
||
contractName: '',
|
||
projectId: '',
|
||
projectName: '',
|
||
organizationId: '',
|
||
organizationName: '',
|
||
signType: '',
|
||
settlementMode: '',
|
||
partyA: '',
|
||
partyB: '',
|
||
handlerUserName: '',
|
||
signDate: '',
|
||
contractFormat: '',
|
||
legalSealFlag: 0,
|
||
copyCount: '',
|
||
settlementCurrency: '',
|
||
invoiceCycle: '',
|
||
paymentDays: '',
|
||
contractAmount: '',
|
||
templateFlag: 0,
|
||
originalContractNo: '',
|
||
electronicSealFlag: 0,
|
||
startDate: '',
|
||
endDate: '',
|
||
temporaryStartDate: '',
|
||
temporaryEndDate: '',
|
||
effectiveType: '',
|
||
feeGenerationMode: 'system',
|
||
archiveStatus: '未归档',
|
||
remark: '',
|
||
});
|
||
// 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息
|
||
const createCopiedDetail = (detail = {}) => {
|
||
const form = defaultForm();
|
||
Object.keys(form).forEach(prop => {
|
||
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
|
||
});
|
||
return {
|
||
...form,
|
||
contractNo: '',
|
||
archiveStatus: '未归档',
|
||
feeGenerationMode: detail.feeGenerationMode || '',
|
||
billingEnabled: detail.billingEnabled,
|
||
contractFileJson: detail.contractFileJson || '',
|
||
attachmentsJson: detail.attachmentsJson || '',
|
||
billingPlanJson: detail.billingPlanJson || '',
|
||
paymentRatioJson: detail.paymentRatioJson || '',
|
||
settlementRuleJson: detail.settlementRuleJson || '',
|
||
preSettlementConfigJson: detail.preSettlementConfigJson || '',
|
||
formalSettlementConfigJson: detail.formalSettlementConfigJson || '',
|
||
changeRecordJson: '',
|
||
};
|
||
};
|
||
const parseArray = value => {
|
||
if (Array.isArray(value)) return value;
|
||
if (!value) return [];
|
||
try {
|
||
const data = JSON.parse(value);
|
||
return Array.isArray(data) ? data : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
};
|
||
const parseObject = value => {
|
||
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
||
if (!value) return {};
|
||
try {
|
||
const data = JSON.parse(value);
|
||
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
};
|
||
const extractRecords = res => {
|
||
const data = res?.data?.data || res?.data || {};
|
||
return Array.isArray(data) ? data : data.records || [];
|
||
};
|
||
const attachmentName = sharedAttachmentName;
|
||
const attachmentUrl = sharedAttachmentUrl;
|
||
const attachmentExtension = row => {
|
||
const source = String(attachmentName(row) || attachmentUrl(row)).split('?')[0];
|
||
const index = source.lastIndexOf('.');
|
||
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
|
||
};
|
||
const isAttachmentImage = row =>
|
||
['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(attachmentExtension(row));
|
||
const isAttachmentPdf = row => {
|
||
const mimeType = String(row.mimeType || row.contentType || '').toLowerCase();
|
||
return mimeType.includes('application/pdf') || attachmentExtension(row) === 'pdf';
|
||
};
|
||
const attachmentViewerPlugins = [
|
||
imagePlugin(),
|
||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||
textPlugin(),
|
||
fallbackPlugin(),
|
||
];
|
||
|
||
export default {
|
||
name: 'ContractManage',
|
||
components: { ContractAttachmentSection, BillingPlanEditor, ElImageViewer, OpenFileViewer, PdfPreview },
|
||
data() {
|
||
return {
|
||
api,
|
||
config,
|
||
tableOption: this.buildTableOption(),
|
||
loading: true,
|
||
data: [],
|
||
query: {},
|
||
page: { currentPage: 1, pageSize: 10, pageSizes: [10, 20, 50, 100], total: 0 },
|
||
selectionList: [],
|
||
searchExpanded: false,
|
||
contractExpiryScope: '',
|
||
contractExpiryStats: { all: 0, within30: 0, within90: 0, over90: 0, expired: 0 },
|
||
expiryTagOptions: [
|
||
{ label: '全部', value: '', statKey: 'all' },
|
||
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
|
||
{ label: '90天内到期', value: 'within90', statKey: 'within90' },
|
||
{ label: '90天以上到期', value: 'over90', statKey: 'over90' },
|
||
{ label: '已到期', value: 'expired', statKey: 'expired' },
|
||
],
|
||
form: defaultForm(),
|
||
formRules: {
|
||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||
contractCategory: [{ required: true, message: '请选择合同类别', trigger: 'change' }],
|
||
signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }],
|
||
settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }],
|
||
projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
|
||
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
|
||
partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }],
|
||
partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }],
|
||
signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
|
||
settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
|
||
contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }],
|
||
legalSealFlag: [{ required: true, message: '请选择是否需要加盖法人章', trigger: 'change' }],
|
||
invoiceCycle: [
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (value === '' || value === undefined || value === null) return callback();
|
||
if (!/^\d+$/.test(String(value)) || Number(value) < 1) {
|
||
return callback(new Error('开票周期只能输入正整数'));
|
||
}
|
||
callback();
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
|
||
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
|
||
},
|
||
submitAction: '',
|
||
projectOptions: [],
|
||
projectLoading: false,
|
||
selectedProjectId: '',
|
||
settlementCurrencyOptions: [],
|
||
settlementModeDictOptions: [],
|
||
contractFormatDictOptions: [],
|
||
contractCategoryDictOptions: [],
|
||
contractPartyAOptions: [],
|
||
contractPartyBOptions: [],
|
||
contractPartyLoading: false,
|
||
contractPartyRequestId: 0,
|
||
organizationOptions: [],
|
||
organizationFlatOptions: [],
|
||
selectedOrganizationId: '',
|
||
organizationLoading: false,
|
||
contractFileRows: [],
|
||
attachmentRows: [],
|
||
attachmentImagePreviewVisible: false,
|
||
attachmentImagePreviewUrls: [],
|
||
attachmentImagePreviewIndex: 0,
|
||
attachmentDocumentPreviewVisible: false,
|
||
attachmentPreviewFile: {},
|
||
attachmentViewerPlugins,
|
||
attachmentViewerToolbar: {
|
||
download: true,
|
||
fullscreen: true,
|
||
print: true,
|
||
rotate: true,
|
||
zoom: true,
|
||
},
|
||
billingPlanRows: [],
|
||
billingPlanBox: false,
|
||
billingPlanIndex: -1,
|
||
billingPlanForm: {},
|
||
settlementConfigTab: 'pre',
|
||
settlementRuleForm: defaultSettlementRule(),
|
||
preSettlementRuleForm: defaultSettlementRule(),
|
||
formalSettlementRuleForm: defaultSettlementRule(),
|
||
paymentRatioRows: [],
|
||
changeRecordRows: [],
|
||
settlementTypeOptions: ['月结', '日结', '周结', '半月结', '固定天数周期结算'],
|
||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||
detailLoading: false,
|
||
detailRow: {},
|
||
detailContractFileRows: [],
|
||
detailAttachmentRows: [],
|
||
detailBillingPlanRows: [],
|
||
detailBillingPlanBox: false,
|
||
detailBillingPlanIndex: -1,
|
||
detailBillingPlanForm: {},
|
||
detailSettlementConfigTab: 'pre',
|
||
detailPreSettlementRuleForm: defaultSettlementRule(),
|
||
detailFormalSettlementRuleForm: defaultSettlementRule(),
|
||
detailPaymentRatioRows: [],
|
||
detailChangeRecordRows: [],
|
||
detailChangeRecordVisible: false,
|
||
detailChangeRecord: null,
|
||
detailChangeRecordDetailRows: [],
|
||
flowBox: false,
|
||
flowUrl: '',
|
||
processInstanceId: '',
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission', 'userInfo']),
|
||
permissionList() {
|
||
return { addBtn: this.canCreate };
|
||
},
|
||
isAdmin() {
|
||
const authority = this.userInfo?.authority;
|
||
return Array.isArray(authority)
|
||
? authority.includes('admin')
|
||
: String(authority || '').includes('admin');
|
||
},
|
||
canCreate() {
|
||
return this.hasPermission(`${this.config.permission}_add`);
|
||
},
|
||
isListPage() {
|
||
return !this.isFormPage && !this.isDetailPage;
|
||
},
|
||
isFormPage() {
|
||
return this.$route.path === '/business/contract-manage/form';
|
||
},
|
||
isDetailPage() {
|
||
return this.$route.path === '/business/contract-manage/detail';
|
||
},
|
||
formMode() {
|
||
return this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||
},
|
||
// 草稿/已撤回/已驳回编辑页:按钮与新增页一致(保存/提交临时/提交正式)
|
||
isDraftEdit() {
|
||
return (
|
||
this.formMode === 'edit' &&
|
||
['draft', 'withdrawn', 'rejected'].includes(this.form.approvalStatus)
|
||
);
|
||
},
|
||
isAttachmentUploadMode() {
|
||
return this.$route.query.attachmentUpload === '1';
|
||
},
|
||
hasApprovedContractFiles() {
|
||
return (this.contractFileRows || []).some(
|
||
row =>
|
||
row?.approved === true ||
|
||
row?.approved === 1 ||
|
||
row?.approved === '1' ||
|
||
row?.approvalStatus === 'approved'
|
||
);
|
||
},
|
||
formPageTitle() {
|
||
if (this.isAttachmentUploadMode) {
|
||
return this.$route.query.name || '附件上传';
|
||
}
|
||
return this.$route.query.name || `${this.formMode === 'edit' ? '编辑' : '新增'}合同管理`;
|
||
},
|
||
detailPageTitle() {
|
||
return this.$route.query.name || '合同详情';
|
||
},
|
||
ids() {
|
||
return this.selectionList.map(item => item.id).join(',');
|
||
},
|
||
contractCategoryOptions() {
|
||
return this.contractCategoryDictOptions.length
|
||
? this.contractCategoryDictOptions
|
||
: this.columnOptions('contractCategory');
|
||
},
|
||
signTypeOptions() {
|
||
return this.columnOptions('signType');
|
||
},
|
||
effectiveTypeOptions() {
|
||
return this.columnOptions('effectiveType');
|
||
},
|
||
settlementModeOptions() {
|
||
return this.settlementModeDictOptions.length
|
||
? this.settlementModeDictOptions
|
||
: this.columnOptions('settlementMode');
|
||
},
|
||
contractFormatOptions() {
|
||
return this.contractFormatDictOptions.length
|
||
? this.contractFormatDictOptions
|
||
: this.columnOptions('contractFormat');
|
||
},
|
||
organizationCascaderProps() {
|
||
return {
|
||
label: 'label',
|
||
value: 'id',
|
||
children: 'children',
|
||
emitPath: true,
|
||
checkStrictly: true,
|
||
expandTrigger: 'click',
|
||
};
|
||
},
|
||
// 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
|
||
// 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新
|
||
organizationCascaderValue: {
|
||
get() {
|
||
const id = this.selectedOrganizationId || this.form.organizationId;
|
||
if (!id) return [];
|
||
return this.findOrganizationPath(id) || [String(id)];
|
||
},
|
||
set(path) {
|
||
const value = Array.isArray(path)
|
||
? path.filter(item => item !== undefined && item !== null && item !== '')
|
||
: [];
|
||
this.selectedOrganizationId = value.length ? String(value.at(-1)) : '';
|
||
this.handleOrganizationChange(value);
|
||
},
|
||
},
|
||
settlementRuleEnabled() {
|
||
return Number(this.settlementRuleForm.autoGenerate) === 1;
|
||
},
|
||
showBillCycleType() {
|
||
return this.settlementRuleForm.settlementType === '月结';
|
||
},
|
||
showBillCutoffDay() {
|
||
return this.showBillCycleType && this.settlementRuleForm.billCycleType === '固定截单日';
|
||
},
|
||
showCustomPeriods() {
|
||
return this.showBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期';
|
||
},
|
||
showCycleDays() {
|
||
return this.settlementRuleForm.settlementType === '固定天数周期结算';
|
||
},
|
||
billCutoffDayOptions() {
|
||
return Array.from({ length: 31 }, (_, index) => ({
|
||
label: `${index + 1}日`,
|
||
value: index + 1,
|
||
}));
|
||
},
|
||
cycleDayOptions() {
|
||
return Array.from({ length: 365 }, (_, index) => ({
|
||
label: `${index + 1}天`,
|
||
value: index + 1,
|
||
}));
|
||
},
|
||
detailContractPeriod() {
|
||
const startDate = this.detailRow.startDate || '-';
|
||
const endDate = this.detailRow.endDate || '-';
|
||
return `${startDate} 至 ${endDate}`;
|
||
},
|
||
detailLegalSealText() {
|
||
const value = this.detailRow.legalSealFlag;
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
return Number(value) === 1 ? '是' : '否';
|
||
},
|
||
detailTemplateText() {
|
||
const value = this.detailRow.templateFlag;
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
return Number(value) === 1 ? '是' : '否';
|
||
},
|
||
detailElectronicSealText() {
|
||
const value = this.detailRow.electronicSealFlag;
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
return Number(value) === 1 ? '是' : '否';
|
||
},
|
||
detailFeeGenerationMode() {
|
||
const value =
|
||
this.detailRow.feeGenerationMode ||
|
||
(Number(this.detailRow.billingEnabled) === 0 ? 'manual' : 'system');
|
||
return value === 'manual' ? '账单导入生成' : '系统生成';
|
||
},
|
||
detailSettlementRule() {
|
||
return this.detailSettlementConfigTab === 'formal'
|
||
? this.detailFormalSettlementRuleForm
|
||
: this.detailPreSettlementRuleForm;
|
||
},
|
||
},
|
||
watch: {
|
||
'$route.fullPath'() {
|
||
if (this.isFormPage) this.initFormPage();
|
||
if (this.isDetailPage) this.initDetailPage();
|
||
},
|
||
settlementConfigTab(tab, oldTab) {
|
||
if (tab === oldTab) return;
|
||
if (oldTab === 'formal') this.formalSettlementRuleForm = { ...this.settlementRuleForm };
|
||
else this.preSettlementRuleForm = { ...this.settlementRuleForm };
|
||
this.settlementRuleForm = {
|
||
...(tab === 'formal' ? this.formalSettlementRuleForm : this.preSettlementRuleForm),
|
||
};
|
||
},
|
||
},
|
||
created() {
|
||
this.loadProjectOptions();
|
||
this.loadSettlementDictionaries();
|
||
this.loadOrganizationOptions();
|
||
if (this.isFormPage) this.initFormPage();
|
||
if (this.isDetailPage) this.initDetailPage();
|
||
},
|
||
methods: {
|
||
buildTableOption() {
|
||
return {
|
||
...option,
|
||
addBtn: false,
|
||
viewBtn: false,
|
||
editBtn: false,
|
||
delBtn: false,
|
||
menuWidth: 320,
|
||
column: (option.column || []).map(column => ({ ...column, display: false })),
|
||
};
|
||
},
|
||
columnOptions(prop) {
|
||
return this.findColumn(this.tableOption.column, prop)?.dicData || [];
|
||
},
|
||
hasPermission(code) {
|
||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||
},
|
||
displayStatus(row, prop) {
|
||
const textProp = prop === this.config.statusProp ? this.config.statusTextProp : '';
|
||
if (textProp && row[textProp]) return row[textProp];
|
||
const item = this.columnOptions(prop).find(dic => String(dic.value) === String(row[prop]));
|
||
return item?.label || row[prop] || '未知';
|
||
},
|
||
statusTagType(status) {
|
||
if (['approved', 'change_approved'].includes(status)) return 'success';
|
||
if (['draft', 'withdrawn'].includes(status)) return 'info';
|
||
if (['rejected', 'change_rejected'].includes(status)) return 'danger';
|
||
return 'warning';
|
||
},
|
||
archiveStatusTagType(status) {
|
||
return status === '已归档' ? 'success' : 'info';
|
||
},
|
||
stageTagType(stage) {
|
||
if (stage === 'formal') return 'success';
|
||
if (stage === 'temporary') return 'warning';
|
||
if (stage === 'terminated') return 'danger';
|
||
return 'info';
|
||
},
|
||
canEdit(row) {
|
||
return (
|
||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||
!row.readonly &&
|
||
(this.config.editStatus || []).includes(row.approvalStatus)
|
||
);
|
||
},
|
||
canDelete(row) {
|
||
return (
|
||
this.hasPermission(`${this.config.permission}_delete`) &&
|
||
!row.readonly &&
|
||
(this.config.deleteStatus || []).includes(row.approvalStatus) &&
|
||
(this.config.deleteStage || []).includes(row.contractStage)
|
||
);
|
||
},
|
||
canCopy(row) {
|
||
return this.hasPermission(`${this.config.permission}_copy`) && !row.readonly;
|
||
},
|
||
editLabel(row) {
|
||
return this.config.editLabelStatusMap?.[row.approvalStatus] || '编辑';
|
||
},
|
||
customOperations(row) {
|
||
return (this.config.operations || []).filter(operation => {
|
||
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
|
||
const statusProp = operation.statusProp || this.config.statusProp || 'status';
|
||
return (
|
||
this.hasPermission(permission) &&
|
||
!row.readonly &&
|
||
(!operation.status?.length || operation.status.includes(row[statusProp])) &&
|
||
(!operation.stage?.length || operation.stage.includes(row.contractStage))
|
||
);
|
||
});
|
||
},
|
||
normalizeSearch(params = {}) {
|
||
const query = { ...params };
|
||
if (query.organizationName) {
|
||
const id = Array.isArray(query.organizationName)
|
||
? query.organizationName.at(-1)
|
||
: query.organizationName;
|
||
const organization = this.findOrganization(id);
|
||
query.organizationId = id;
|
||
if (organization) query.organizationName = organization.rawLabel || organization.label;
|
||
}
|
||
return query;
|
||
},
|
||
onLoad(page, params = {}) {
|
||
this.loading = true;
|
||
const query = this.normalizeSearch({
|
||
...params,
|
||
...this.query,
|
||
...(this.contractExpiryScope ? { expireScope: this.contractExpiryScope } : {}),
|
||
});
|
||
this.api
|
||
.getList(page.currentPage, page.pageSize, query)
|
||
.then(res => {
|
||
const result = res.data?.data || {};
|
||
this.page.total = result.total || 0;
|
||
this.data = result.records || [];
|
||
this.selectionList = [];
|
||
this.refreshExpiryStats(query);
|
||
})
|
||
.finally(() => {
|
||
this.loading = false;
|
||
});
|
||
},
|
||
searchChange(params, done) {
|
||
this.query = { ...params };
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
done();
|
||
},
|
||
searchReset() {
|
||
this.query = {};
|
||
this.contractExpiryScope = '';
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
},
|
||
selectionChange(list) {
|
||
this.selectionList = list;
|
||
},
|
||
currentChange(currentPage) {
|
||
this.page.currentPage = currentPage;
|
||
},
|
||
sizeChange(pageSize) {
|
||
this.page.pageSize = pageSize;
|
||
},
|
||
refreshChange() {
|
||
this.onLoad(this.page, this.query);
|
||
},
|
||
changeExpiryScope(value) {
|
||
this.contractExpiryScope = value;
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page, this.query);
|
||
},
|
||
refreshExpiryStats(query) {
|
||
const params = { ...query };
|
||
delete params.expireScope;
|
||
this.api.expireStats(params).then(res => {
|
||
this.contractExpiryStats = { ...this.contractExpiryStats, ...(res.data?.data || {}) };
|
||
});
|
||
},
|
||
openForm(mode, row = {}) {
|
||
this.$router.push({
|
||
path: '/business/contract-manage/form',
|
||
query:
|
||
mode === 'edit'
|
||
? { mode, id: row.id, name: '编辑合同管理' }
|
||
: { mode, name: '新增合同管理' },
|
||
});
|
||
},
|
||
initFormPage() {
|
||
this.resetFormState();
|
||
if (this.formMode === 'add') {
|
||
const copyId = this.$route.query.copyId;
|
||
if (copyId) {
|
||
this.api.getDetail(copyId).then(res => this.applyCopyDetail(res.data?.data || {}));
|
||
return;
|
||
}
|
||
this.loadContractPartyOptions();
|
||
return;
|
||
}
|
||
const id = this.$route.query.id;
|
||
if (!id) return;
|
||
this.api.getDetail(id).then(res => this.applyDetail(res.data?.data || {}));
|
||
},
|
||
resetFormState() {
|
||
this.form = {
|
||
...defaultForm(),
|
||
handlerUserName:
|
||
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '',
|
||
};
|
||
this.selectedProjectId = '';
|
||
this.selectedOrganizationId = '';
|
||
this.contractPartyAOptions = [];
|
||
this.contractPartyBOptions = [];
|
||
this.contractFileRows = [];
|
||
this.attachmentRows = [];
|
||
this.billingPlanRows = [];
|
||
this.preSettlementRuleForm = defaultSettlementRule();
|
||
this.formalSettlementRuleForm = defaultSettlementRule();
|
||
this.settlementConfigTab = 'pre';
|
||
this.settlementRuleForm = this.preSettlementRuleForm;
|
||
this.paymentRatioRows = [];
|
||
this.changeRecordRows = [];
|
||
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
|
||
},
|
||
applyDetail(detail) {
|
||
const projectId = normalizeProjectValue(detail.projectId);
|
||
const projectName = projectId ? normalizeProjectValue(detail.projectName) : '';
|
||
this.form = {
|
||
...defaultForm(),
|
||
...detail,
|
||
projectId,
|
||
projectName,
|
||
copyCount: normalizeOptionalInteger(detail.copyCount, ''),
|
||
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''),
|
||
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
|
||
contractAmount: normalizeOptionalAmount(detail.contractAmount, ''),
|
||
archiveStatus: detail.archiveStatus || '未归档',
|
||
feeGenerationMode:
|
||
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
|
||
};
|
||
this.selectedProjectId = projectId;
|
||
this.selectedOrganizationId = detail.organizationId || '';
|
||
this.contractFileRows = normalizeContractFileRows(parseArray(detail.contractFileJson));
|
||
this.attachmentRows = parseArray(detail.attachmentsJson);
|
||
this.billingPlanRows = parseArray(detail.billingPlanJson);
|
||
this.paymentRatioRows = parseArray(detail.paymentRatioJson);
|
||
this.changeRecordRows = parseArray(detail.changeRecordJson);
|
||
const payload = parseObject(detail.settlementRuleJson);
|
||
const legacy = Object.keys(payload).some(
|
||
key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)
|
||
)
|
||
? payload
|
||
: {};
|
||
const preConfig = parseObject(detail.preSettlementConfigJson);
|
||
const formalConfig = parseObject(detail.formalSettlementConfigJson);
|
||
this.preSettlementRuleForm = this.normalizeSettlementRule(
|
||
payload.preSettlementConfig || (Object.keys(preConfig).length ? preConfig : legacy)
|
||
);
|
||
this.formalSettlementRuleForm = this.normalizeSettlementRule(
|
||
payload.formalSettlementConfig || (Object.keys(formalConfig).length ? formalConfig : legacy)
|
||
);
|
||
this.settlementConfigTab = 'pre';
|
||
this.settlementRuleForm = { ...this.preSettlementRuleForm };
|
||
this.syncCurrentProjectOption();
|
||
this.syncCurrentOrganizationOption();
|
||
this.loadContractPartyOptions();
|
||
},
|
||
applyCopyDetail(detail) {
|
||
this.applyDetail(createCopiedDetail(detail));
|
||
this.form.handlerUserName =
|
||
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
|
||
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
|
||
},
|
||
closeForm() {
|
||
this.$router.$avueRouter?.closeTag?.();
|
||
this.$router.push('/business/contract-manage');
|
||
},
|
||
validateBase(action) {
|
||
if (action === 'temporary') {
|
||
const required = [
|
||
['signType', '签约类型'],
|
||
['contractName', '合同名称'],
|
||
['contractCategory', '合同类型'],
|
||
['projectName', '所属项目'],
|
||
['organizationName', '所属组织'],
|
||
['partyA', '甲方'],
|
||
['partyB', '乙方'],
|
||
];
|
||
const empty = required.find(([prop]) => {
|
||
const value = this.form[prop];
|
||
return value === undefined || value === null || String(value).trim() === '';
|
||
});
|
||
if (empty) {
|
||
this.$message.warning(
|
||
empty[0] === 'contractName' ? `请输入${empty[1]}` : `请选择${empty[1]}`
|
||
);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
if (!this.form.contractName?.trim()) {
|
||
this.$message.warning('请输入合同名称');
|
||
return false;
|
||
}
|
||
if (!this.form.contractCategory) {
|
||
this.$message.warning('请选择合同类别');
|
||
return false;
|
||
}
|
||
if (!this.form.settlementCurrency) {
|
||
this.$message.warning('请选择结算币种');
|
||
return false;
|
||
}
|
||
if (action === 'draft') return true;
|
||
if (['formal', 'edit'].includes(action) && !this.form.signType) {
|
||
this.$message.warning('请选择签约类型');
|
||
return false;
|
||
}
|
||
if (action !== 'formal' && action !== 'edit') return true;
|
||
const required = [
|
||
['projectName', '所属项目'],
|
||
['organizationName', '所属组织'],
|
||
['partyA', '甲方'],
|
||
['partyB', '乙方'],
|
||
['startDate', '合同开始日期'],
|
||
['endDate', '合同结束日期'],
|
||
['signDate', '签订日期'],
|
||
['settlementMode', '结算方式'],
|
||
['contractFormat', '合同格式'],
|
||
];
|
||
const empty = required.find(([prop]) => !this.form[prop]);
|
||
if (empty) {
|
||
this.$message.warning(`请选择或输入${empty[1]}`);
|
||
return false;
|
||
}
|
||
return (
|
||
this.validateBillingPlanDefault() &&
|
||
this.validateSettlementRules() &&
|
||
this.validatePaymentRatios()
|
||
);
|
||
},
|
||
normalizeSubmit() {
|
||
this.syncSettlementConfig();
|
||
const projectId = normalizeProjectValue(this.form.projectId);
|
||
const projectName = projectId ? normalizeProjectValue(this.form.projectName) : '';
|
||
return {
|
||
...this.form,
|
||
projectId,
|
||
projectName,
|
||
copyCount: normalizeOptionalInteger(this.form.copyCount),
|
||
invoiceCycle: normalizeOptionalInteger(this.form.invoiceCycle),
|
||
paymentDays: normalizeOptionalInteger(this.form.paymentDays),
|
||
contractAmount: normalizeOptionalAmount(this.form.contractAmount),
|
||
billingEnabled: this.form.feeGenerationMode === 'manual' ? 0 : 1,
|
||
contractFileJson: JSON.stringify(this.contractFileRows),
|
||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||
billingPlanJson: JSON.stringify(this.billingPlanRows),
|
||
settlementRuleJson: JSON.stringify({
|
||
preSettlementConfig: this.normalizeSettlementRule(this.preSettlementRuleForm),
|
||
formalSettlementConfig: this.normalizeSettlementRule(this.formalSettlementRuleForm),
|
||
}),
|
||
preSettlementConfigJson: JSON.stringify(
|
||
this.normalizeSettlementRule(this.preSettlementRuleForm)
|
||
),
|
||
formalSettlementConfigJson: JSON.stringify(
|
||
this.normalizeSettlementRule(this.formalSettlementRuleForm)
|
||
),
|
||
paymentRatioJson: JSON.stringify(this.paymentRatioRows),
|
||
changeRecordJson: JSON.stringify(this.changeRecordRows),
|
||
};
|
||
},
|
||
submitCreate(action) {
|
||
if (this.submitAction || !this.validateBase(action)) return;
|
||
this.submitAction = action;
|
||
const submit = {
|
||
...this.normalizeSubmit(),
|
||
contractStage: action === 'formal' ? 'temporary' : 'draft',
|
||
approvalStatus: 'draft',
|
||
};
|
||
this.api
|
||
.saveDraft(submit)
|
||
.then(res => {
|
||
const saved = res.data?.data || {};
|
||
const id = saved.id || this.form.id;
|
||
if (action === 'temporary' || action === 'formal') {
|
||
if (!id) throw new Error('合同保存成功但未返回主键,无法提交合同');
|
||
return action === 'temporary'
|
||
? this.api.toTemporary(id)
|
||
: this.api.submitFormal(id);
|
||
}
|
||
return null;
|
||
})
|
||
.then(() => {
|
||
const message = {
|
||
draft: '保存成功',
|
||
temporary: '临时合同提交成功',
|
||
formal: '正式合同提交成功',
|
||
}[action];
|
||
this.$message.success(message);
|
||
this.closeForm();
|
||
})
|
||
.finally(() => {
|
||
this.submitAction = '';
|
||
});
|
||
},
|
||
submitEdit() {
|
||
if (this.submitAction || !this.validateBase('edit')) return;
|
||
this.submitAction = 'edit';
|
||
const isChangeRejected = this.form.approvalStatus === 'change_rejected';
|
||
const payload = this.normalizeSubmit();
|
||
const request = isChangeRejected
|
||
? this.api.submitChange(payload)
|
||
: this.api.submit(payload);
|
||
request
|
||
.then(() => {
|
||
this.$message.success(isChangeRejected ? '已重新提交变更审批' : '修改成功');
|
||
this.closeForm();
|
||
})
|
||
.finally(() => {
|
||
this.submitAction = '';
|
||
});
|
||
},
|
||
submitAttachmentUpload() {
|
||
if (this.submitAction) return;
|
||
if (!this.form.id) {
|
||
this.$message.warning('合同不存在,无法上传附件');
|
||
return;
|
||
}
|
||
this.submitAction = 'attachment';
|
||
this.api
|
||
.updateAttachments({
|
||
id: this.form.id,
|
||
contractFileJson: JSON.stringify(this.contractFileRows),
|
||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||
})
|
||
.then(() => {
|
||
this.$message.success('附件保存成功');
|
||
this.closeForm();
|
||
})
|
||
.finally(() => {
|
||
this.submitAction = '';
|
||
});
|
||
},
|
||
loadProjectOptions() {
|
||
if (this.projectLoading) return;
|
||
this.projectLoading = true;
|
||
getProjectList(1, 9999, this.config.projectQueryParams || {})
|
||
.then(res => {
|
||
this.projectOptions = extractRecords(res);
|
||
this.syncCurrentProjectOption();
|
||
})
|
||
.finally(() => {
|
||
this.projectLoading = false;
|
||
});
|
||
},
|
||
loadSettlementDictionaries() {
|
||
Promise.all([
|
||
getSystemDictionary({ code: 'currency_type' }),
|
||
getBizDictionary({ code: 'settle_method' }),
|
||
getBizDictionary({ code: 'contractFormat' }),
|
||
getBizDictionary({ code: 'contractCategory' }),
|
||
]).then(([currencyRes, methodRes, formatRes, categoryRes]) => {
|
||
const records = response => {
|
||
const data = response?.data?.data || response?.data || [];
|
||
return Array.isArray(data) ? data : data.records || [];
|
||
};
|
||
this.settlementCurrencyOptions = records(currencyRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
this.settlementModeDictOptions = records(methodRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
this.contractFormatDictOptions = records(formatRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
this.contractCategoryDictOptions = records(categoryRes).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
['settlementMode'].forEach(prop => {
|
||
const column = this.findColumn(this.tableOption.column, prop);
|
||
if (column) column.dicData = this.settlementModeDictOptions;
|
||
});
|
||
const formatColumn = this.findColumn(this.tableOption.column, 'contractFormat');
|
||
if (formatColumn && this.contractFormatDictOptions.length) {
|
||
formatColumn.dicData = this.contractFormatDictOptions;
|
||
}
|
||
const categoryColumn = this.findColumn(this.tableOption.column, 'contractCategory');
|
||
if (categoryColumn && this.contractCategoryDictOptions.length) {
|
||
categoryColumn.dicData = this.contractCategoryDictOptions;
|
||
}
|
||
});
|
||
},
|
||
syncCurrentProjectOption() {
|
||
if (
|
||
!this.form.projectId ||
|
||
!this.form.projectName ||
|
||
String(this.form.projectId) === '-1' ||
|
||
String(this.form.projectName) === '-1'
|
||
) {
|
||
return;
|
||
}
|
||
if (this.projectOptions.some(item => String(item.id) === String(this.form.projectId))) return;
|
||
this.projectOptions.unshift({ id: this.form.projectId, projectName: this.form.projectName });
|
||
},
|
||
handleProjectChange(projectId) {
|
||
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
|
||
if (!project) {
|
||
Object.assign(this.form, { projectId: '', projectName: '', settlementMode: '' });
|
||
return;
|
||
}
|
||
Object.assign(this.form, {
|
||
projectId: project.id,
|
||
projectName: project.projectName || '',
|
||
settlementMode: project.settlementMode || this.form.settlementMode || '',
|
||
});
|
||
},
|
||
loadContractPartyOptions() {
|
||
if (this.contractPartyLoading) return;
|
||
const requestId = ++this.contractPartyRequestId;
|
||
this.contractPartyLoading = true;
|
||
const queryParams = { status: 1, approvalStatus: 'approved' };
|
||
Promise.all([
|
||
getCustomerArchiveList(1, 9999, queryParams),
|
||
getCustomerArchiveList(1, 9999, { ...queryParams, accessType: 'temporary' }),
|
||
])
|
||
.then(responses => {
|
||
if (requestId !== this.contractPartyRequestId) return;
|
||
const names = responses
|
||
.flatMap(extractRecords)
|
||
.map(item => item.fullName || item.shortName || item.customerName || item.customerCode)
|
||
.map(name => String(name || '').trim())
|
||
.filter(Boolean);
|
||
[this.form.partyA, this.form.partyB].filter(Boolean).forEach(name => names.unshift(name));
|
||
const options = [...new Set(names)].map(name => ({ label: name, value: name }));
|
||
this.contractPartyAOptions = options;
|
||
this.contractPartyBOptions = options;
|
||
})
|
||
.finally(() => {
|
||
if (requestId === this.contractPartyRequestId) this.contractPartyLoading = false;
|
||
});
|
||
},
|
||
loadOrganizationOptions() {
|
||
if (this.organizationLoading) return;
|
||
this.organizationLoading = true;
|
||
getDeptTree(this.userInfo?.tenantId)
|
||
.then(res => {
|
||
const organizationTree = this.excludeExternalOrganization(res.data?.data || []);
|
||
this.organizationOptions = this.normalizeOrganizationTree(organizationTree);
|
||
this.organizationFlatOptions = this.flattenOrganizationTree(this.organizationOptions);
|
||
const column = this.findColumn(this.tableOption.column, 'organizationName');
|
||
if (column) {
|
||
column.dicData = this.organizationOptions;
|
||
column.renderSearch = scope => this.renderOrganizationSearch(scope);
|
||
}
|
||
this.syncCurrentOrganizationOption();
|
||
})
|
||
.finally(() => {
|
||
this.organizationLoading = false;
|
||
});
|
||
},
|
||
excludeExternalOrganization(tree = []) {
|
||
return tree.reduce((result, item) => {
|
||
const organizationName = item.title || item.deptName || item.name || item.label || '';
|
||
if (String(organizationName).trim() === '外部组织') return result;
|
||
result.push({
|
||
...item,
|
||
children: this.excludeExternalOrganization(item.children || []),
|
||
});
|
||
return result;
|
||
}, []);
|
||
},
|
||
normalizeOrganizationTree(tree) {
|
||
return (tree || []).map(item => {
|
||
const label = item.title || item.deptName || item.name || item.label || '';
|
||
const children = this.normalizeOrganizationTree(item.children || []);
|
||
return {
|
||
...item,
|
||
id: String(item.id),
|
||
label,
|
||
rawLabel: label,
|
||
children: children.length ? children : undefined,
|
||
};
|
||
});
|
||
},
|
||
flattenOrganizationTree(tree) {
|
||
return (tree || []).flatMap(item => [
|
||
{ id: String(item.id), label: item.label, rawLabel: item.rawLabel },
|
||
...this.flattenOrganizationTree(item.children || []),
|
||
]);
|
||
},
|
||
findOrganization(id) {
|
||
const value = Array.isArray(id) ? id.at(-1) : id;
|
||
return this.organizationFlatOptions.find(item => String(item.id) === String(value));
|
||
},
|
||
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
||
findOrganizationPath(id, tree = this.organizationOptions, parents = []) {
|
||
const target = Array.isArray(id) ? id.at(-1) : id;
|
||
if (target === undefined || target === null || target === '') return null;
|
||
for (const node of tree || []) {
|
||
const path = [...parents, String(node.id)];
|
||
if (String(node.id) === String(target)) return path;
|
||
const matched = this.findOrganizationPath(target, node.children, path);
|
||
if (matched) return matched;
|
||
}
|
||
return null;
|
||
},
|
||
handleOrganizationChange(id) {
|
||
const organization = this.findOrganization(id);
|
||
Object.assign(
|
||
this.form,
|
||
organization
|
||
? {
|
||
organizationId: organization.id,
|
||
organizationName: organization.rawLabel || organization.label,
|
||
}
|
||
: { organizationId: '', organizationName: '' }
|
||
);
|
||
},
|
||
syncCurrentOrganizationOption() {
|
||
if (!this.form.organizationId || !this.form.organizationName) return;
|
||
if (this.findOrganization(this.form.organizationId)) return;
|
||
const item = {
|
||
id: String(this.form.organizationId),
|
||
label: this.form.organizationName,
|
||
rawLabel: this.form.organizationName,
|
||
};
|
||
this.organizationOptions.unshift(item);
|
||
this.organizationFlatOptions.unshift(item);
|
||
},
|
||
renderOrganizationSearch(scope) {
|
||
// 搜索区所属组织:单选级联(与新增/编辑表单一致)
|
||
// 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称
|
||
return h(ElCascader, {
|
||
modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [],
|
||
'onUpdate:modelValue': value => {
|
||
// 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中
|
||
if (scope.row) scope.row.organizationName = Array.isArray(value) ? value : [];
|
||
},
|
||
options: this.organizationOptions,
|
||
props: {
|
||
label: 'label',
|
||
value: 'id',
|
||
children: 'children',
|
||
checkStrictly: true,
|
||
expandTrigger: 'click',
|
||
},
|
||
filterable: true,
|
||
clearable: true,
|
||
style: 'width: 100%',
|
||
placeholder: '请选择 所属组织',
|
||
});
|
||
},
|
||
integerInput(prop, value) {
|
||
this.form[prop] = String(value || '').replace(/\D/g, '');
|
||
},
|
||
positiveIntegerInput(prop, value) {
|
||
const normalized = String(value || '').replace(/\D/g, '').replace(/^0+/, '');
|
||
this.form[prop] = normalized;
|
||
},
|
||
previewAttachment(row, rows = []) {
|
||
const url = attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空,无法预览');
|
||
return;
|
||
}
|
||
if (isAttachmentImage(row)) {
|
||
this.attachmentImagePreviewUrls = (rows || [])
|
||
.filter(item => isAttachmentImage(item) && attachmentUrl(item))
|
||
.map(item => attachmentUrl(item));
|
||
this.attachmentImagePreviewIndex = Math.max(
|
||
this.attachmentImagePreviewUrls.indexOf(url),
|
||
0
|
||
);
|
||
this.attachmentImagePreviewVisible = true;
|
||
return;
|
||
}
|
||
const isPdf = isAttachmentPdf(row);
|
||
this.attachmentPreviewFile = {
|
||
name: attachmentName(row),
|
||
url,
|
||
mimeType: isPdf ? 'application/pdf' : row.mimeType || row.contentType || '',
|
||
isPdf,
|
||
};
|
||
this.attachmentDocumentPreviewVisible = true;
|
||
},
|
||
handleAttachmentPreviewUnsupported() {
|
||
this.$message.warning('当前文件暂不支持在线预览');
|
||
},
|
||
handleAttachmentPreviewError() {
|
||
this.$message.error('附件预览失败');
|
||
},
|
||
openBillingPlan(row = {}, index = -1) {
|
||
this.billingPlanIndex = index;
|
||
this.billingPlanForm =
|
||
index < 0
|
||
? { planName: '', transportMode: '', defaultPlan: false, remark: '', rules: [] }
|
||
: clone(row);
|
||
this.billingPlanBox = true;
|
||
this.$nextTick(() => this.$refs.billingPlanEditor?.ensureTransportModes?.());
|
||
},
|
||
saveBillingPlan(plan, index) {
|
||
const nextPlan = clone(plan);
|
||
if (this.isDefaultBillingPlan(nextPlan)) {
|
||
this.billingPlanRows.forEach((item, itemIndex) => {
|
||
if (itemIndex !== index && item.transportMode === nextPlan.transportMode) {
|
||
item.defaultPlan = false;
|
||
}
|
||
});
|
||
}
|
||
if (index < 0) this.billingPlanRows.push(nextPlan);
|
||
else this.billingPlanRows.splice(index, 1, nextPlan);
|
||
this.billingPlanBox = false;
|
||
},
|
||
removeBillingPlan(index) {
|
||
this.$confirm('确定删除该计费方案?', '提示', { type: 'warning' }).then(() => {
|
||
this.billingPlanRows.splice(index, 1);
|
||
});
|
||
},
|
||
isDefaultBillingPlan(plan) {
|
||
return (
|
||
plan?.defaultPlan === true ||
|
||
plan?.defaultPlan === 1 ||
|
||
['true', '1'].includes(String(plan?.defaultPlan).toLowerCase())
|
||
);
|
||
},
|
||
validateBillingPlanDefault() {
|
||
if (!this.billingPlanRows.length) return true;
|
||
const defaultCountByTransportMode = this.billingPlanRows.reduce((result, plan) => {
|
||
const transportMode = plan.transportMode || '';
|
||
result[transportMode] =
|
||
(result[transportMode] || 0) + Number(this.isDefaultBillingPlan(plan));
|
||
return result;
|
||
}, {});
|
||
const invalidTransportMode = Object.entries(defaultCountByTransportMode).find(
|
||
([, count]) => count > 1
|
||
);
|
||
if (invalidTransportMode) {
|
||
const [transportMode, count] = invalidTransportMode;
|
||
this.$message.warning(`运输方式“${transportMode}”仅支持配置一个默认计费方案`);
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
normalizeSettlementRule(rule = {}) {
|
||
const next = { ...defaultSettlementRule(), ...rule };
|
||
if (next.settlementType !== '月结') {
|
||
next.billCycleType = '';
|
||
next.billCutoffDay = '';
|
||
next.customPeriods = [];
|
||
} else if (next.billCycleType !== '固定截单日') {
|
||
next.billCutoffDay = '';
|
||
}
|
||
if (next.settlementType === '月结' && next.billCycleType === '自定义多周期') {
|
||
next.customPeriods = normalizeCustomPeriods(
|
||
Array.isArray(next.customPeriods) ? next.customPeriods : []
|
||
);
|
||
} else {
|
||
next.customPeriods = [];
|
||
}
|
||
if (next.settlementType !== '固定天数周期结算') next.cycleDays = '';
|
||
return next;
|
||
},
|
||
syncSettlementConfig() {
|
||
if (this.settlementConfigTab === 'formal')
|
||
this.formalSettlementRuleForm = { ...this.settlementRuleForm };
|
||
else this.preSettlementRuleForm = { ...this.settlementRuleForm };
|
||
},
|
||
handleSettlementTypeChange(value) {
|
||
if (value === '月结') {
|
||
if (this.settlementRuleForm.billCycleType === '自定义多周期') {
|
||
this.settlementRuleForm.customPeriods = normalizeCustomPeriods(
|
||
this.settlementRuleForm.customPeriods || []
|
||
);
|
||
} else {
|
||
this.settlementRuleForm.customPeriods = [];
|
||
}
|
||
this.settlementRuleForm.cycleDays = '';
|
||
return;
|
||
}
|
||
this.settlementRuleForm.billCycleType = '';
|
||
this.settlementRuleForm.billCutoffDay = '';
|
||
this.settlementRuleForm.customPeriods = [];
|
||
if (value !== '固定天数周期结算') this.settlementRuleForm.cycleDays = '';
|
||
},
|
||
handleBillCycleTypeChange(value) {
|
||
if (value !== '固定截单日') this.settlementRuleForm.billCutoffDay = '';
|
||
if (value === '自定义多周期') {
|
||
this.settlementRuleForm.customPeriods = normalizeCustomPeriods(
|
||
this.settlementRuleForm.customPeriods || []
|
||
);
|
||
return;
|
||
}
|
||
this.settlementRuleForm.customPeriods = [];
|
||
},
|
||
customPeriodEndDayOptions(row) {
|
||
const startDay = Number(row?.startDay);
|
||
if (!Number.isFinite(startDay) || startDay < 1) return this.billCutoffDayOptions;
|
||
return Array.from({ length: 31 - startDay + 1 }, (_, index) => {
|
||
const value = startDay + index;
|
||
return { label: `${value}日`, value };
|
||
});
|
||
},
|
||
handleCustomPeriodStartDayChange(index, value) {
|
||
const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []);
|
||
const row = periods[index];
|
||
if (!row) return;
|
||
row.startDay = value || '';
|
||
const startDay = Number(row.startDay);
|
||
const endDay = Number(row.endDay);
|
||
if (
|
||
Number.isFinite(startDay) &&
|
||
Number.isFinite(endDay) &&
|
||
(endDay < startDay || endDay > 31)
|
||
) {
|
||
row.endDay = '';
|
||
}
|
||
this.settlementRuleForm.customPeriods = periods;
|
||
},
|
||
handleCustomPeriodEndDayChange(index, value) {
|
||
const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []);
|
||
const row = periods[index];
|
||
if (!row) return;
|
||
row.endDay = value || '';
|
||
this.settlementRuleForm.customPeriods = periods;
|
||
},
|
||
addCustomPeriodRow() {
|
||
const periods = normalizeCustomPeriods(this.settlementRuleForm.customPeriods || []);
|
||
periods.push({ startDay: '', endDay: '' });
|
||
this.settlementRuleForm.customPeriods = periods;
|
||
},
|
||
removeCustomPeriodRow(index) {
|
||
if (index <= 0) return;
|
||
const periods = [...(this.settlementRuleForm.customPeriods || [])];
|
||
periods.splice(index, 1);
|
||
this.settlementRuleForm.customPeriods = normalizeCustomPeriods(periods);
|
||
},
|
||
validateCustomPeriods(periods = [], label) {
|
||
const rows = normalizeCustomPeriods(periods);
|
||
if (!rows.length) {
|
||
this.$message.warning(`${label}:请至少配置一段自定义周期`);
|
||
return false;
|
||
}
|
||
for (let index = 0; index < rows.length; index += 1) {
|
||
const row = rows[index];
|
||
const startDay = Number(row.startDay);
|
||
const endDay = Number(row.endDay);
|
||
if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) {
|
||
this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`);
|
||
return false;
|
||
}
|
||
if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) {
|
||
this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`);
|
||
return false;
|
||
}
|
||
if (endDay < startDay) {
|
||
this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`);
|
||
return false;
|
||
}
|
||
if (index > 0) {
|
||
const previousEnd = Number(rows[index - 1].endDay);
|
||
if (startDay !== previousEnd + 1) {
|
||
this.$message.warning(
|
||
`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`
|
||
);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
},
|
||
validateSettlementRule(rule, label) {
|
||
if (Number(rule.autoGenerate) !== 1) return true;
|
||
if (!rule.billStartDate || !rule.settlementType) {
|
||
this.$message.warning(`${label}:请完整填写账单起始日期和结算类型`);
|
||
return false;
|
||
}
|
||
if (rule.settlementType === '月结' && !rule.billCycleType) {
|
||
this.$message.warning(`${label}:请选择结算周期`);
|
||
return false;
|
||
}
|
||
if (
|
||
rule.settlementType === '月结' &&
|
||
rule.billCycleType === '固定截单日' &&
|
||
!rule.billCutoffDay
|
||
) {
|
||
this.$message.warning(`${label}:请选择账单截单日`);
|
||
return false;
|
||
}
|
||
if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') {
|
||
return this.validateCustomPeriods(rule.customPeriods, label);
|
||
}
|
||
if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) {
|
||
this.$message.warning(`${label}:请选择周期天数`);
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
validateSettlementRules() {
|
||
this.syncSettlementConfig();
|
||
return (
|
||
this.validateSettlementRule(this.preSettlementRuleForm, '预结算配置') &&
|
||
this.validateSettlementRule(this.formalSettlementRuleForm, '正式结算配置')
|
||
);
|
||
},
|
||
addPaymentRatio() {
|
||
this.paymentRatioRows.push({
|
||
paymentTerm: `第${this.paymentRatioRows.length + 1}笔`,
|
||
ratioLimit: '',
|
||
remark: '',
|
||
});
|
||
},
|
||
validatePaymentRatios() {
|
||
const total = this.paymentRatioRows.reduce(
|
||
(sum, row) => sum + Number(row.ratioLimit || 0),
|
||
0
|
||
);
|
||
if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) {
|
||
this.$message.warning('付款比例上限合计必须等于100%');
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
ratioInput(row, value) {
|
||
let nextValue = String(value ?? '').replace(/[^\d.]/g, '');
|
||
const parts = nextValue.split('.');
|
||
if (parts.length > 2) nextValue = `${parts.shift()}.${parts.join('')}`;
|
||
const [integerPart, decimalPart] = nextValue.split('.');
|
||
nextValue =
|
||
decimalPart === undefined ? integerPart : `${integerPart}.${decimalPart.slice(0, 2)}`;
|
||
if (Number(nextValue) > 100) nextValue = '100';
|
||
row.ratioLimit = nextValue;
|
||
},
|
||
openDetail(row) {
|
||
this.$router.push({
|
||
path: '/business/contract-manage/detail',
|
||
query: { id: row.id, name: '合同详情' },
|
||
});
|
||
},
|
||
initDetailPage() {
|
||
const id = this.$route.query.id;
|
||
if (!id) return;
|
||
this.detailLoading = true;
|
||
this.applyDetailState({});
|
||
this.api
|
||
.getDetail(id)
|
||
.then(res => {
|
||
this.applyDetailState(res.data?.data || {});
|
||
})
|
||
.finally(() => {
|
||
this.detailLoading = false;
|
||
});
|
||
},
|
||
closeDetail() {
|
||
this.$router.$avueRouter?.closeTag?.();
|
||
this.$router.push('/business/contract-manage');
|
||
},
|
||
applyDetailState(detail = {}) {
|
||
this.detailRow = {
|
||
...detail,
|
||
copyCount: normalizeOptionalInteger(detail.copyCount),
|
||
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle),
|
||
paymentDays: normalizeOptionalInteger(detail.paymentDays),
|
||
};
|
||
this.detailContractFileRows = normalizeContractFileRows(parseArray(detail.contractFileJson));
|
||
this.detailAttachmentRows = parseArray(detail.attachmentsJson);
|
||
this.detailBillingPlanRows = parseArray(detail.billingPlanJson);
|
||
this.detailPaymentRatioRows = parseArray(detail.paymentRatioJson);
|
||
this.detailChangeRecordRows = parseArray(detail.changeRecordJson);
|
||
const payload = parseObject(detail.settlementRuleJson);
|
||
const legacy = Object.keys(payload).some(
|
||
key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)
|
||
)
|
||
? payload
|
||
: {};
|
||
const preConfig = parseObject(detail.preSettlementConfigJson);
|
||
const formalConfig = parseObject(detail.formalSettlementConfigJson);
|
||
this.detailPreSettlementRuleForm = this.normalizeSettlementRule(
|
||
payload.preSettlementConfig || (Object.keys(preConfig).length ? preConfig : legacy)
|
||
);
|
||
this.detailFormalSettlementRuleForm = this.normalizeSettlementRule(
|
||
payload.formalSettlementConfig || (Object.keys(formalConfig).length ? formalConfig : legacy)
|
||
);
|
||
this.detailSettlementConfigTab = 'pre';
|
||
},
|
||
openDetailBillingPlan(row, index) {
|
||
this.detailBillingPlanForm = clone(row);
|
||
this.detailBillingPlanIndex = index;
|
||
this.detailBillingPlanBox = true;
|
||
},
|
||
parseChangeRecordData(value) {
|
||
if (!value) return {};
|
||
if (typeof value === 'object') return value;
|
||
try {
|
||
const data = JSON.parse(value);
|
||
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
||
} catch (error) {
|
||
return {};
|
||
}
|
||
},
|
||
normalizeChangeRecordValue(value) {
|
||
if (typeof value !== 'string') return value;
|
||
const text = value.trim();
|
||
if (!text || (!text.startsWith('[') && !text.startsWith('{') && text !== 'null')) {
|
||
return value;
|
||
}
|
||
try {
|
||
return JSON.parse(text);
|
||
} catch (error) {
|
||
return value;
|
||
}
|
||
},
|
||
isEmptyChangeRecordValue(value) {
|
||
const normalized = this.normalizeChangeRecordValue(value);
|
||
if (normalized === undefined || normalized === null || normalized === '') return true;
|
||
if (Array.isArray(normalized)) return normalized.length === 0;
|
||
if (typeof normalized === 'object') return Object.keys(normalized).length === 0;
|
||
return false;
|
||
},
|
||
getContractChangeFieldLabel(field) {
|
||
const labels = {
|
||
contractName: '合同名称',
|
||
partyB: '乙方',
|
||
startDate: '开始日期',
|
||
endDate: '结束日期',
|
||
contractFormat: '合同格式',
|
||
settlementMode: '结算方式',
|
||
legalSealFlag: '是否需要加盖法人章',
|
||
copyCount: '一式(份)',
|
||
settlementCurrency: '结算币种',
|
||
invoiceCycle: '开票周期',
|
||
paymentDays: '回款账期',
|
||
contractAmount: '合同金额',
|
||
templateFlag: '是否范本',
|
||
originalContractNo: '原件合同编号',
|
||
electronicSealFlag: '是否电子章',
|
||
remark: '备注',
|
||
feeGenerationMode: '费用生成模式',
|
||
billingPlanJson: '计费方案',
|
||
settlementRuleJson: '结算单规则',
|
||
preSettlementConfigJson: '预结算配置',
|
||
formalSettlementConfigJson: '正式结算配置',
|
||
paymentRatioJson: '付款比例设置',
|
||
contractFileJson: '合同文件',
|
||
attachmentsJson: '其它附件',
|
||
changeAttachmentsJson: '变更材料',
|
||
};
|
||
return labels[field] || field;
|
||
},
|
||
formatChangeRecordAttachments(value) {
|
||
const attachments = Array.isArray(value)
|
||
? value
|
||
: parseArray(typeof value === 'string' ? value : JSON.stringify(value || []));
|
||
const names = attachments
|
||
.map(item =>
|
||
typeof item === 'string'
|
||
? item
|
||
: item?.originalName || item?.name || item?.fileName || item?.url || item?.link || ''
|
||
)
|
||
.filter(Boolean);
|
||
return names.length ? names.join('、') : '空';
|
||
},
|
||
formatChangeRecordBillingPlans(value) {
|
||
const list = Array.isArray(value) ? value : [];
|
||
if (!list.length) return '空';
|
||
return list
|
||
.map(item => {
|
||
if (!item || typeof item !== 'object') return String(item ?? '');
|
||
const name = item.planName || item.name || '未命名方案';
|
||
return item.defaultPlan ? `${name}(默认)` : name;
|
||
})
|
||
.filter(Boolean)
|
||
.join('、');
|
||
},
|
||
formatChangeRecordPaymentRatios(value) {
|
||
const list = Array.isArray(value) ? value : [];
|
||
if (!list.length) return '空';
|
||
return list
|
||
.map(item => {
|
||
if (!item || typeof item !== 'object') return String(item ?? '');
|
||
const term = item.paymentTerm || '';
|
||
const ratio = item.ratioLimit === undefined || item.ratioLimit === null || item.ratioLimit === '' ? '' : `${item.ratioLimit}%`;
|
||
return [term, ratio].filter(Boolean).join(' ');
|
||
})
|
||
.filter(Boolean)
|
||
.join(';');
|
||
},
|
||
formatChangeRecordSettlementRule(value) {
|
||
const normalized = this.normalizeChangeRecordValue(value);
|
||
if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) {
|
||
return this.isEmptyChangeRecordValue(normalized) ? '空' : String(normalized);
|
||
}
|
||
const summary = config => {
|
||
if (!config || typeof config !== 'object') return '';
|
||
const parts = [];
|
||
const autoGenerate = config.autoGenerate;
|
||
if (autoGenerate !== undefined && autoGenerate !== null && autoGenerate !== '') {
|
||
parts.push(`自动生成结算单:${Number(autoGenerate) === 1 ? '开启' : '关闭'}`);
|
||
}
|
||
if (config.settlementType) parts.push(`结算类型:${config.settlementType}`);
|
||
if (config.billCycleType) parts.push(`结算周期:${config.billCycleType}`);
|
||
if (config.billCutoffDay) parts.push(`账单截单日:${config.billCutoffDay}日`);
|
||
if (config.cycleDays) parts.push(`周期天数:${config.cycleDays}天`);
|
||
if (
|
||
config.billCycleType === '自定义多周期' &&
|
||
Array.isArray(config.customPeriods) &&
|
||
config.customPeriods.length
|
||
) {
|
||
parts.push(
|
||
`自定义周期:${config.customPeriods
|
||
.map(item => `${item.startDay || '-'}-${item.endDay || '-'}日`)
|
||
.join(',')}`
|
||
);
|
||
}
|
||
if (config.billStartDate) parts.push(`账单起始日期:${config.billStartDate}`);
|
||
return parts.join(',');
|
||
};
|
||
if (normalized.preSettlementConfig || normalized.formalSettlementConfig) {
|
||
return [
|
||
`预结算:${summary(normalized.preSettlementConfig) || '空'}`,
|
||
`正式结算:${summary(normalized.formalSettlementConfig) || '空'}`,
|
||
].join(';');
|
||
}
|
||
return summary(normalized) || '空';
|
||
},
|
||
formatContractChangeValue(field, value) {
|
||
const normalized = this.normalizeChangeRecordValue(value);
|
||
if (this.isEmptyChangeRecordValue(normalized)) return '空';
|
||
if (['contractFileJson', 'attachmentsJson', 'changeAttachmentsJson'].includes(field)) {
|
||
return this.formatChangeRecordAttachments(normalized);
|
||
}
|
||
if (field === 'billingPlanJson') return this.formatChangeRecordBillingPlans(normalized);
|
||
if (field === 'paymentRatioJson') return this.formatChangeRecordPaymentRatios(normalized);
|
||
if (
|
||
['settlementRuleJson', 'preSettlementConfigJson', 'formalSettlementConfigJson'].includes(
|
||
field
|
||
)
|
||
) {
|
||
return this.formatChangeRecordSettlementRule(normalized);
|
||
}
|
||
if (['legalSealFlag', 'templateFlag', 'electronicSealFlag'].includes(field)) {
|
||
return Number(normalized) === 1 ? '是' : '否';
|
||
}
|
||
if (field === 'feeGenerationMode') return normalized === 'manual' ? '账单导入生成' : '系统生成';
|
||
if (field === 'invoiceCycle' || field === 'paymentDays') return `${normalized}天`;
|
||
if (Array.isArray(normalized) || typeof normalized === 'object') {
|
||
return JSON.stringify(normalized);
|
||
}
|
||
return String(normalized);
|
||
},
|
||
// 列表「变更内容」列:把变更前后内容拼成一段摘要(参照客商变更记录)
|
||
formatContractChangeContent(row = {}) {
|
||
const rows = this.buildDetailChangeRecordRows(row).filter(
|
||
item => item.field !== '变更原因'
|
||
);
|
||
return rows
|
||
.map(item => `${item.field}:变更前:${item.before} → 变更后:${item.after}`)
|
||
.join(';');
|
||
},
|
||
buildDetailChangeRecordRows(row = {}) {
|
||
const beforeData = this.parseChangeRecordData(row.beforeData);
|
||
const afterData = this.parseChangeRecordData(row.afterData);
|
||
const fields = [...new Set([...Object.keys(beforeData), ...Object.keys(afterData)])];
|
||
const rows = fields
|
||
.filter(field => {
|
||
// 前后都为空的字段不展示('' 与 null 序列化不同,单靠 JSON 比对会漏掉)
|
||
if (
|
||
this.isEmptyChangeRecordValue(beforeData[field]) &&
|
||
this.isEmptyChangeRecordValue(afterData[field])
|
||
) {
|
||
return false;
|
||
}
|
||
return (
|
||
JSON.stringify(this.normalizeChangeRecordValue(beforeData[field])) !==
|
||
JSON.stringify(this.normalizeChangeRecordValue(afterData[field]))
|
||
);
|
||
})
|
||
.map(field => ({
|
||
field: this.getContractChangeFieldLabel(field),
|
||
before: this.formatContractChangeValue(field, beforeData[field]),
|
||
after: this.formatContractChangeValue(field, afterData[field]),
|
||
}));
|
||
if (row.changeContent) {
|
||
rows.unshift({ field: '变更内容', before: '空', after: row.changeContent });
|
||
}
|
||
if (row.changeReason) {
|
||
rows.push({ field: '变更原因', before: '空', after: row.changeReason });
|
||
}
|
||
return rows;
|
||
},
|
||
openDetailChangeRecord(row) {
|
||
this.detailChangeRecord = row;
|
||
this.detailChangeRecordDetailRows = this.buildDetailChangeRecordRows(row);
|
||
this.detailChangeRecordVisible = true;
|
||
},
|
||
displayValue(value) {
|
||
return value === undefined || value === null || value === '' ? '-' : value;
|
||
},
|
||
detailUnitValue(prop, unit) {
|
||
const value = this.detailRow[prop];
|
||
return value === undefined || value === null || value === '' || Number(value) < 0
|
||
? ''
|
||
: `${value}${unit}`;
|
||
},
|
||
detailObjectUnitValue(row, prop, unit) {
|
||
const value = row?.[prop];
|
||
return value === undefined || value === null || value === '' ? '-' : `${value}${unit}`;
|
||
},
|
||
detailValue(prop) {
|
||
const value = this.detailRow[prop];
|
||
if (
|
||
prop === 'contractStage' ||
|
||
prop === 'approvalStatus' ||
|
||
prop === 'contractCategory' ||
|
||
prop === 'signType' ||
|
||
prop === 'effectiveType' ||
|
||
prop === 'contractFormat'
|
||
) {
|
||
return this.displayStatus(this.detailRow, prop);
|
||
}
|
||
if (prop === 'billingPlanJson') return `${parseArray(value).length}个计费方案`;
|
||
if (prop === 'settlementRuleJson') return value ? '已配置' : '-';
|
||
if (prop === 'paymentRatioJson') return `${parseArray(value).length}项付款比例`;
|
||
if (prop === 'feeGenerationMode') return value === 'manual' ? '账单导入生成' : '系统生成';
|
||
return this.displayValue(value);
|
||
},
|
||
detailDictionaryValue(prop, options = []) {
|
||
const value = this.detailRow[prop];
|
||
if (value === undefined || value === null || value === '') return '-';
|
||
return options.find(item => String(item.value) === String(value))?.label || value;
|
||
},
|
||
handleOperation(operation, row) {
|
||
if (operation.action === 'startChange') {
|
||
this.$router.push({
|
||
path: '/business/contract-manage/change',
|
||
query: { id: row.id, name: '合同变更' },
|
||
});
|
||
return;
|
||
}
|
||
if (operation.action === 'flow') {
|
||
this.openFlow(row);
|
||
return;
|
||
}
|
||
if (operation.action === 'attachmentUpload') {
|
||
this.$router.push({
|
||
path: '/business/contract-manage/form',
|
||
query: {
|
||
mode: 'edit',
|
||
id: row.id,
|
||
name: '附件上传',
|
||
attachmentUpload: '1',
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
const run = value => {
|
||
const args = operation.prompt ? [row.id, value] : [row.id];
|
||
this.api[operation.action](...args).then(() => {
|
||
this.$message.success(operation.successMessage || `${operation.label}成功`);
|
||
this.onLoad(this.page, this.query);
|
||
});
|
||
};
|
||
if (operation.prompt) {
|
||
this.$prompt(operation.prompt, operation.label, {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
inputPattern: /\S+/,
|
||
inputErrorMessage: '请输入内容',
|
||
}).then(({ value }) => run(value));
|
||
} else {
|
||
this.$confirm(operation.confirmMessage || `确定${operation.label}该合同?`, '提示', {
|
||
type: 'warning',
|
||
}).then(() => run());
|
||
}
|
||
},
|
||
removeRow(row) {
|
||
this.$confirm('确定删除该合同?', '提示', { type: 'warning' })
|
||
.then(() => this.api.remove(row.id))
|
||
.then(() => {
|
||
this.$message.success('删除成功');
|
||
this.onLoad(this.page, this.query);
|
||
});
|
||
},
|
||
handleCopy(row) {
|
||
this.$router.push({
|
||
path: '/business/contract-manage/form',
|
||
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
|
||
});
|
||
},
|
||
openFlow(row, loaded = false) {
|
||
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
|
||
if (!processInstanceId && !loaded && row.id) {
|
||
this.api.getDetail(row.id).then(res => this.openFlow(res.data?.data || {}, true));
|
||
return;
|
||
}
|
||
if (!processInstanceId) {
|
||
this.$message.warning('流程实例为空,无法查看流程');
|
||
return;
|
||
}
|
||
if (this.website.design.designMode) this.processInstanceId = processInstanceId;
|
||
else
|
||
this.flowUrl = `/api/blade-flow/process/diagram-view?processInstanceId=${processInstanceId}`;
|
||
this.flowBox = true;
|
||
},
|
||
handleExport() {
|
||
this.$confirm(`是否导出${this.config.exportName}?`, '提示', { type: 'warning' }).then(() => {
|
||
NProgress.start();
|
||
exportBlob(
|
||
this.config.exportUrl,
|
||
{
|
||
...this.normalizeSearch(this.query),
|
||
ids: this.ids,
|
||
[this.website.tokenHeader]: getToken(),
|
||
},
|
||
{ feedback: true }
|
||
)
|
||
.then(res =>
|
||
downloadXls(
|
||
res.data,
|
||
`${this.config.exportName}${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
|
||
)
|
||
)
|
||
.finally(() => NProgress.done());
|
||
});
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.contract-name-cell {
|
||
display: inline-block;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
vertical-align: bottom;
|
||
}
|
||
|
||
.contract-manage-page {
|
||
&__expiry-search {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
&__footer {
|
||
position: fixed;
|
||
z-index: 20;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 230px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
min-height: 64px;
|
||
box-sizing: border-box;
|
||
padding: 12px 24px;
|
||
background: #fff;
|
||
border-top: 1px solid #eff1f7;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
|
||
// 统一 12px 间距,与全站底部操作栏(去 gap,由相邻按钮 margin 提供)一致
|
||
.el-button + .el-button {
|
||
margin-left: 12px;
|
||
}
|
||
}
|
||
|
||
&__flow {
|
||
width: 100%;
|
||
height: calc(100vh - 150px);
|
||
border: 0;
|
||
}
|
||
|
||
:deep(.avue-crud__header) {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
:deep(.avue-crud__menu) {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
// 搜索区域白底卡片包裹
|
||
:deep(.avue-crud__search) {
|
||
background-color: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
:deep(.avue-crud__search:not(.el-card)) {
|
||
padding: 12px 12px 4px;
|
||
}
|
||
|
||
&--form {
|
||
min-height: 100%;
|
||
padding-bottom: 72px;
|
||
background: #f5f6fa;
|
||
|
||
:deep(.basic-container__card) {
|
||
border: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.contract-change-record-content-cell {
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.contract-change-record-content-tooltip {
|
||
max-width: 900px;
|
||
max-height: 320px;
|
||
overflow: auto;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.contract-change-record-detail-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px 32px;
|
||
margin-bottom: 16px;
|
||
color: #606266;
|
||
}
|
||
|
||
:deep(.contract-change-record-detail-dialog .el-dialog__body) {
|
||
max-height: 65vh;
|
||
overflow: auto;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
:deep(.contract-change-record-detail-dialog .contract-change-record-detail-value .cell) {
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
:global(.avue--collapse .contract-manage-page__footer) {
|
||
left: 60px;
|
||
}
|
||
|
||
:global(.avue-layout--horizontal .contract-manage-page__footer) {
|
||
left: 0;
|
||
}
|
||
|
||
.contract-manage-form {
|
||
//padding: 12px 16px 24px;
|
||
background: #f5f6fa;
|
||
|
||
&--attachment-upload {
|
||
:deep(.contract-manage-form__section--panel:not(.contract-manage-form__section--attachment)) {
|
||
pointer-events: none;
|
||
opacity: 0.92;
|
||
}
|
||
|
||
:deep(.contract-manage-form__section--attachment) {
|
||
pointer-events: auto;
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
&__section {
|
||
margin-bottom: 12px;
|
||
|
||
&--panel {
|
||
padding: 14px 16px 16px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
> .dialog-section-title,
|
||
:deep(.dialog-section-title) {
|
||
margin-bottom: 20px;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
&__section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px;
|
||
|
||
.dialog-section-title {
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
&__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
column-gap: 48px;
|
||
}
|
||
|
||
&__remark {
|
||
width: 100%;
|
||
margin-bottom: 0 !important;
|
||
}
|
||
|
||
&__date-range {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 360px;
|
||
max-width: 100%;
|
||
|
||
span {
|
||
color: #606266;
|
||
}
|
||
|
||
:deep(.el-date-editor) {
|
||
width: auto;
|
||
min-width: 0;
|
||
}
|
||
}
|
||
|
||
&__billing-head,
|
||
&__attachment-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
&__settlement-switch {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
margin-bottom: 16px;
|
||
|
||
.settlement-switch-tip {
|
||
margin: 0 4px;
|
||
color: #a8abb2;
|
||
cursor: help;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
&__settlement-grid {
|
||
:deep(.el-form-item__label) {
|
||
flex-basis: 120px !important;
|
||
width: 120px !important;
|
||
white-space: nowrap;
|
||
word-break: normal;
|
||
}
|
||
}
|
||
|
||
&__custom-periods {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
&__billing-head > div {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
&__fee-mode-tip {
|
||
margin-left: -22px;
|
||
margin-right: 20px;
|
||
color: #a8abb2;
|
||
cursor: help;
|
||
font-size: 14px;
|
||
}
|
||
|
||
&__tip {
|
||
margin: 0 0 8px;
|
||
color: #303133;
|
||
}
|
||
|
||
&__ratio-unit {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
align-items: center;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
:deep(.el-form-item__label) {
|
||
flex: 0 0 96px !important;
|
||
width: 96px !important;
|
||
height: auto;
|
||
padding-right: 12px;
|
||
white-space: normal;
|
||
word-break: break-all;
|
||
line-height: 18px;
|
||
}
|
||
|
||
:deep(.el-form-item__content) {
|
||
min-width: 0;
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-cascader),
|
||
:deep(.el-tree-select),
|
||
:deep(.el-input-number),
|
||
:deep(.el-date-editor) {
|
||
width: 360px;
|
||
max-width: 100%;
|
||
}
|
||
|
||
:deep(.el-tabs__header) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
--el-table-border-color: #eff1f7;
|
||
}
|
||
|
||
:deep(.el-table th.el-table__cell) {
|
||
height: 42px;
|
||
padding: 8px 0;
|
||
background: #fafafa;
|
||
}
|
||
|
||
:deep(.el-table td.el-table__cell) {
|
||
height: 52px;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
:deep(.el-link + .el-link) {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
:deep(.vehicle-attachment-upload) {
|
||
width: auto;
|
||
}
|
||
}
|
||
|
||
:deep(.contract-manage-form__attachment-head) {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
|
||
// 标题与「批量下载」按钮同排、垂直居中,消除标题自身下边距
|
||
.dialog-section-title {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
:deep(.contract-manage-form__attachment-upload) {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
margin-top: 12px;
|
||
|
||
:deep(.vehicle-attachment-upload) {
|
||
display: flex;
|
||
align-items: center;
|
||
width: auto;
|
||
}
|
||
|
||
:deep(.el-upload) {
|
||
display: inline-flex;
|
||
}
|
||
|
||
:deep(.el-upload__tip) {
|
||
display: inline-block;
|
||
margin: 0 0 0 30px;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
text-align: left;
|
||
}
|
||
}
|
||
|
||
.contract-manage-detail {
|
||
&__descriptions {
|
||
:deep(.el-descriptions__label) {
|
||
display: inline-block;
|
||
width: 96px;
|
||
min-width: 96px;
|
||
max-width: 96px;
|
||
box-sizing: border-box;
|
||
padding-right: 12px;
|
||
color: #606266;
|
||
line-height: 18px;
|
||
text-align: right;
|
||
white-space: normal;
|
||
word-break: break-all;
|
||
}
|
||
|
||
:deep(.el-descriptions__content) {
|
||
color: #303133;
|
||
word-break: break-word;
|
||
}
|
||
|
||
:deep(.el-descriptions__cell) {
|
||
padding-bottom: 16px;
|
||
}
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.contract-manage-form__grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
column-gap: 32px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.contract-manage-form {
|
||
padding: 12px;
|
||
|
||
&__grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
.el-dialog .el-form-item__label {
|
||
white-space: normal;
|
||
line-height: 1.2;
|
||
}
|
||
</style>
|