Files
tms-erp-web/src/views/business/contract-manage.vue
T
b2894lxlx cb98355715 Merge remote-tracking branch 'websoft/master'
# Conflicts:
#	src/views/business/contract-manage-change.vue
2026-08-25 20:47:06 +08:00

2374 lines
81 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container :class="['contract-manage-page', { 'contract-manage-page--form': isFormPage }]">
<template v-if="!isFormPage">
<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 #search-menu>
<div v-show="searchExpanded" class="contract-manage-page__expiry-search">
<span>签约类型</span>
<el-select v-model="contractSignType" clearable placeholder="请选择">
<el-option
v-for="item in signTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<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" @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 #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>
<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"
>
<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="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="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="所属组织" prop="organizationName">
<el-cascader
v-model="selectedOrganizationId"
:options="organizationOptions"
:props="organizationCascaderProps"
placeholder="请选择"
clearable
filterable
:show-all-levels="false"
@visible-change="visible => visible && loadOrganizationOptions()"
@change="handleOrganizationChange"
/>
</el-form-item>
<el-form-item label="经办人">
<el-input v-model="form.handlerUserName" disabled />
</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="settlementMode">
<el-select v-model="form.settlementMode" placeholder="请选择结算类型">
<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="合同格式" 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="是否需要加盖法人章" 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="一式">
<el-input
v-model="form.copyCount"
placeholder="请输入"
@input="value => integerInput('copyCount', value)"
/>
</el-form-item>
<el-form-item label="回款账期">
<el-input
v-model="form.paymentDays"
placeholder="请输入"
@input="value => integerInput('paymentDays', value)"
/>
</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>
<attachment-section
title="合同文件"
: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-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-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>
</section>
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="contract-manage-form__section-head">
<div class="dialog-section-title">付款比例设置</div>
<el-button type="primary" plain @click="addPaymentRatio">添加</el-button>
</div>
<p class="contract-manage-form__tip">非必填;配置付款比例时,合计必须等于100%。</p>
<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>
<attachment-section
title="其它附件"
description
: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
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="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="formMode === 'add'">
<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"
>
<open-file-viewer
v-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="detailBox"
title="合同详情"
append-to-body
destroy-on-close
class="contract-manage-detail-dialog"
width="92%"
top="4vh"
>
<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="签订日期">{{
detailValue('signDate')
}}</el-descriptions-item>
<el-descriptions-item label="结算类型">{{
detailValue('settlementMode')
}}</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="回款账期">{{
detailUnitValue('paymentDays', '天')
}}</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{
detailValue('remark')
}}</el-descriptions-item>
</el-descriptions>
</section>
<attachment-section
title="合同文件"
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>
</section>
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">付款比例设置</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>
<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
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="openFlow(row)">流程</el-link></template
>
</el-table-column>
</el-table>
</section>
</div>
<template #footer
><el-button type="primary" @click="detailBox = 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 { defineComponent, h, resolveComponent } from 'vue';
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 { 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 { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import {
fallbackPlugin,
imagePlugin,
officePlugin,
pdfPlugin,
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 defaultSettlementRule = () => ({
autoGenerate: 1,
billStartDate: '',
settlementType: '月结',
billCycleType: '固定截单日',
billCutoffDay: 25,
cycleDays: '',
});
const defaultForm = () => ({
contractCategory: '客户合同',
contractNo: '',
contractName: '',
projectId: '',
projectName: '',
organizationId: '',
organizationName: '',
signType: '',
settlementMode: '',
partyA: '',
partyB: '',
handlerUserName: '',
signDate: '',
contractFormat: '',
legalSealFlag: 0,
copyCount: '',
paymentDays: '',
startDate: '',
endDate: '',
temporaryStartDate: '',
temporaryEndDate: '',
effectiveType: '',
feeGenerationMode: 'system',
remark: '',
});
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 = row => row.originalName || row.name || row.fileName || '附件';
const attachmentUrl = row =>
row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || '';
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 attachmentFileTypes = [
'pdf',
'bmp',
'jpeg',
'png',
'jpg',
'doc',
'docx',
'ppt',
'pptx',
'xlsx',
'xls',
'eml',
'msg',
'zip',
];
const attachmentViewerPlugins = [
imagePlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
const AttachmentSection = defineComponent({
name: 'AttachmentSection',
props: {
title: { type: String, required: true },
rows: { type: Array, default: () => [] },
description: Boolean,
readonly: Boolean,
preview: { type: Function, default: null },
},
emits: ['update:rows'],
data() {
return { selected: [], attachmentFileTypes };
},
methods: {
update(rows) {
this.$emit('update:rows', rows);
},
handleChange(rows) {
const uploadUserName = this.$store.getters.userInfo?.realName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.update(
(rows || []).map(row => ({
...row,
description: row.description || '',
uploadUserName: row.uploadUserName || uploadUserName,
uploadTime: row.uploadTime || uploadTime,
}))
);
},
remove(index) {
const rows = [...this.rows];
rows.splice(index, 1);
this.update(rows);
},
download(row) {
const url = attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空,无法下载');
return;
}
downloadFileByUrl(url, attachmentName(row));
},
batchDownload() {
(this.selected.length ? this.selected : this.rows).forEach(this.download);
},
formatSize(size) {
const value = Number(size);
if (!value) return '';
if (value < 1024) return `${value}B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
return `${(value / 1024 / 1024).toFixed(1)}MB`;
},
},
render() {
const ElButton = resolveComponent('el-button');
const ElInput = resolveComponent('el-input');
const ElLink = resolveComponent('el-link');
const ElTable = resolveComponent('el-table');
const ElTableColumn = resolveComponent('el-table-column');
const VehicleAttachmentUpload = resolveComponent('vehicle-attachment-upload');
const columns = [
...(!this.readonly
? [h(ElTableColumn, { type: 'selection', width: 55, align: 'center' })]
: []),
h(ElTableColumn, { type: 'index', label: '序号', width: 70, align: 'center' }),
h(
ElTableColumn,
{
label: '文件名',
minWidth: 240,
align: 'left',
headerAlign: 'left',
showOverflowTooltip: true,
},
{
default: ({ row }) =>
h(
ElLink,
{
type: 'primary',
onClick: () => this.preview?.(row, this.rows),
},
() => attachmentName(row)
),
}
),
];
if (this.description) {
columns.push(
h(
ElTableColumn,
{ label: '附件描述', minWidth: 220 },
{
default: ({ row }) =>
this.readonly
? row.description || '-'
: h(ElInput, {
modelValue: row.description,
maxlength: 200,
'onUpdate:modelValue': value => {
row.description = value;
this.update([...this.rows]);
},
}),
}
)
);
}
columns.push(
h(
ElTableColumn,
{ label: '文件大小', width: 120, align: 'center' },
{
default: ({ row }) => this.formatSize(row.size),
}
),
h(ElTableColumn, { prop: 'uploadUserName', label: '上传人', width: 140, align: 'center' }),
h(ElTableColumn, {
prop: 'uploadTime',
label: '上传时间',
width: 180,
align: 'center',
sortable: true,
})
);
if (!this.readonly) {
columns.push(
h(
ElTableColumn,
{ label: '操作', width: 100, align: 'center', fixed: 'right' },
{
default: ({ $index }) =>
h(ElLink, { type: 'danger', onClick: () => this.remove($index) }, () => '删除'),
}
)
);
}
return h(
'section',
{
class: 'contract-manage-form__section contract-manage-form__section--panel',
},
[
h('div', { class: 'dialog-section-title' }, this.title),
h('div', { class: 'contract-manage-form__attachment-head' }, [
h(
ElButton,
{ type: 'primary', disabled: !this.rows.length, onClick: this.batchDownload },
() => '批量下载'
),
]),
h(
ElTable,
{
data: this.rows,
border: true,
class: 'contract-manage-form__attachment-table',
onSelectionChange: rows => {
this.selected = rows;
},
},
() => columns
),
...(!this.readonly
? [
h('div', { class: 'contract-manage-form__attachment-upload' }, [
h(VehicleAttachmentUpload, {
modelValue: this.rows,
fileTypes: this.attachmentFileTypes,
maxSize: 500,
showTip: true,
tip: '支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过500M',
showFileList: false,
buttonText: '上传附件',
'onUpdate:modelValue': this.handleChange,
onChange: this.handleChange,
}),
]),
]
: []),
]
);
},
});
export default {
name: 'ContractManage',
components: { AttachmentSection, BillingPlanEditor, ElImageViewer, OpenFileViewer },
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,
contractSignType: '',
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' }],
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' }],
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
},
submitAction: '',
projectOptions: [],
projectLoading: false,
selectedProjectId: '',
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: ['固定截单日', '自然月'],
detailBox: false,
detailLoading: false,
detailRow: {},
detailContractFileRows: [],
detailAttachmentRows: [],
detailBillingPlanRows: [],
detailBillingPlanBox: false,
detailBillingPlanIndex: -1,
detailBillingPlanForm: {},
detailSettlementConfigTab: 'pre',
detailPreSettlementRuleForm: defaultSettlementRule(),
detailFormalSettlementRuleForm: defaultSettlementRule(),
detailPaymentRatioRows: [],
detailChangeRecordRows: [],
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`);
},
isFormPage() {
return this.$route.path === '/business/contract-manage/form';
},
formMode() {
return this.$route.query.mode === 'edit' ? 'edit' : 'add';
},
formPageTitle() {
return this.$route.query.name || `${this.formMode === 'edit' ? '编辑' : '新增'}合同管理`;
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
contractCategoryOptions() {
return this.columnOptions('contractCategory');
},
signTypeOptions() {
return this.columnOptions('signType');
},
effectiveTypeOptions() {
return this.columnOptions('effectiveType');
},
settlementModeOptions() {
return this.columnOptions('settlementMode');
},
contractFormatOptions() {
return this.columnOptions('contractFormat');
},
organizationCascaderProps() {
return {
label: 'label',
value: 'id',
children: 'children',
checkStrictly: true,
emitPath: false,
};
},
settlementRuleEnabled() {
return Number(this.settlementRuleForm.autoGenerate) === 1;
},
showBillCycleType() {
return this.settlementRuleForm.settlementType === '月结';
},
showBillCutoffDay() {
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 ? '是' : '否';
},
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();
},
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.loadOrganizationOptions();
if (this.isFormPage) this.initFormPage();
},
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';
},
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 &&
typeof this.api.copy === 'function'
);
},
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.contractSignType ? { signType: this.contractSignType } : {}),
...(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.contractSignType = '';
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') {
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) {
this.form = {
...defaultForm(),
...detail,
feeGenerationMode:
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
};
this.selectedProjectId = detail.projectId || '';
this.selectedOrganizationId = detail.organizationId || '';
this.contractFileRows = 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();
},
closeForm() {
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/business/contract-manage');
},
validateBase(action) {
if (!this.form.contractName?.trim()) {
this.$message.warning('请输入合同名称');
return false;
}
if (!this.form.contractCategory) {
this.$message.warning('请选择合同类别');
return false;
}
if (['temporary', '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();
return {
...this.form,
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 === 'draft' ? 'draft' : 'temporary',
approvalStatus: 'draft',
};
this.api
.saveDraft(submit)
.then(res => {
const saved = res.data?.data || {};
const id = saved.id || this.form.id;
if (action === 'formal') {
if (!id) throw new Error('合同保存成功但未返回主键,无法提交正式合同');
return 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';
this.api
.submit(this.normalizeSubmit())
.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;
});
},
syncCurrentProjectOption() {
if (!this.form.projectId || !this.form.projectName) 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: '' });
return;
}
Object.assign(this.form, {
projectId: project.id,
projectName: project.projectName || '',
});
},
loadContractPartyOptions() {
if (this.contractPartyLoading) return;
const requestId = ++this.contractPartyRequestId;
this.contractPartyLoading = true;
getCustomerArchiveList(1, 9999, { status: 1, approvalStatus: 'approved' })
.then(res => {
if (requestId !== this.contractPartyRequestId) return;
const names = extractRecords(res)
.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 => {
this.organizationOptions = this.normalizeOrganizationTree(res.data?.data || []);
this.organizationFlatOptions = this.flattenOrganizationTree(this.organizationOptions);
const column = this.findColumn(this.tableOption.column, 'organizationName');
if (column) column.dicData = this.organizationOptions;
this.syncCurrentOrganizationOption();
})
.finally(() => {
this.organizationLoading = false;
});
},
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));
},
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);
},
integerInput(prop, value) {
this.form[prop] = String(value || '').replace(/\D/g, '');
},
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;
}
this.attachmentPreviewFile = {
name: attachmentName(row),
url,
mimeType: row.mimeType || row.contentType || '',
};
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 = '';
} else if (next.billCycleType !== '固定截单日') next.billCutoffDay = '';
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 === '月结') {
this.settlementRuleForm.billCycleType ||= '固定截单日';
if (this.settlementRuleForm.billCycleType === '固定截单日')
this.settlementRuleForm.billCutoffDay ||= 25;
this.settlementRuleForm.cycleDays = '';
return;
}
this.settlementRuleForm.billCycleType = '';
this.settlementRuleForm.billCutoffDay = '';
if (value !== '固定天数周期结算') this.settlementRuleForm.cycleDays = '';
},
handleBillCycleTypeChange(value) {
this.settlementRuleForm.billCutoffDay =
value === '固定截单日' ? this.settlementRuleForm.billCutoffDay || 25 : '';
},
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.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.detailBox = true;
this.detailLoading = true;
this.applyDetailState(row);
this.api
.getDetail(row.id)
.then(res => {
this.applyDetailState(res.data?.data || row);
})
.finally(() => {
this.detailLoading = false;
});
},
applyDetailState(detail = {}) {
this.detailRow = { ...detail };
this.detailContractFileRows = 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;
},
displayValue(value) {
return value === undefined || value === null || value === '' ? '-' : value;
},
detailUnitValue(prop, unit) {
const value = this.detailRow[prop];
return value === undefined || value === null || value === '' ? '-' : `${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'
) {
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);
},
handleOperation(operation, row) {
if (operation.action === 'startChange') {
this.$router.push({ path: '/business/contract-manage/change', query: { id: row.id } });
return;
}
if (operation.action === 'flow') {
this.openFlow(row);
return;
}
if (operation.action === 'attachmentUpload') {
this.openForm('edit', row);
return;
}
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
this.api[operation.action](...args).then(() => {
this.$message.success(`${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.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.$confirm('确定复制该合同?', '提示', { type: 'warning' })
.then(() => this.api.copy(row.id))
.then(() => {
this.$message.success('复制成功');
this.onLoad(this.page, this.query);
});
},
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-manage-page {
&__expiry-search {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding-top: 8px;
.el-select {
width: 180px;
}
}
&__footer {
position: fixed;
z-index: 20;
right: 0;
bottom: 0;
left: 230px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
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);
}
&__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;
}
&--form {
min-height: 100%;
padding-bottom: 72px;
background: #f5f6fa;
:deep(.basic-container__card) {
border: 0;
background: transparent;
box-shadow: none;
}
}
}
: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;
&__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 {
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-grid {
:deep(.el-form-item__label) {
flex-basis: 120px !important;
width: 120px !important;
white-space: nowrap;
word-break: normal;
}
}
&__billing-head > div {
display: flex;
align-items: center;
gap: 20px;
}
&__tip {
margin: 0 0 8px;
color: #303133;
}
&__ratio-unit {
margin-left: 8px;
}
:deep(.el-form-item) {
align-items: center;
margin-bottom: 16px;
}
: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-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: flex-end;
margin-bottom: 16px;
}
: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 {
max-height: 74vh;
overflow-y: auto;
&__descriptions {
:deep(.el-descriptions__label) {
display: inline-block;
min-width: 96px;
padding-right: 12px;
color: #606266;
text-align: right;
}
: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;
}
.contract-manage-form__tip {
color: #ff0000 !important;
font-size: 12px;
}
</style>