cb98355715
# Conflicts: # src/views/business/contract-manage-change.vue
2374 lines
81 KiB
Vue
2374 lines
81 KiB
Vue
<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>
|