1881 lines
62 KiB
Vue
1881 lines
62 KiB
Vue
<template>
|
||
<basic-container class="project-apply-page">
|
||
<avue-crud
|
||
:option="tableOption"
|
||
:table-loading="loading"
|
||
:data="data"
|
||
v-model:page="page"
|
||
v-model="tableForm"
|
||
ref="crud"
|
||
:permission="permissionList"
|
||
@search-change="searchChange"
|
||
@search-reset="searchReset"
|
||
@selection-change="selectionChange"
|
||
@current-change="currentChange"
|
||
@size-change="sizeChange"
|
||
@refresh-change="refreshChange"
|
||
@on-load="onLoad"
|
||
>
|
||
<template #menu-left>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-plus"
|
||
v-if="canMajorProjectSupplement"
|
||
@click="openProjectDialog('majorSupplement')"
|
||
>
|
||
重大项目补录
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-plus"
|
||
v-if="canCreate"
|
||
@click="openProjectDialog('add')"
|
||
>
|
||
新增
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-download"
|
||
plain
|
||
v-if="config.exportUrl && hasPermission(`${config.permission}_export`)"
|
||
@click="handleExport"
|
||
>
|
||
批量导出
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
icon="el-icon-delete"
|
||
plain
|
||
v-if="hasPermission(`${config.permission}_delete`) && !readonlyScope"
|
||
@click="handleDelete"
|
||
>
|
||
批量删除
|
||
</el-button>
|
||
</template>
|
||
|
||
<template #approvalStatus="{ row }">
|
||
<el-tag :type="statusTagType(row.approvalStatus)">
|
||
{{ displayStatus(row, 'approvalStatus') }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #menu="{ row, index }">
|
||
<el-link
|
||
type="primary"
|
||
v-if="hasPermission(`${config.permission}_view`)"
|
||
@click="openProjectDialog('view', row)"
|
||
>
|
||
查看
|
||
</el-link>
|
||
<el-link type="primary" v-if="canEdit(row)" @click="openProjectDialog('edit', row)">
|
||
编辑
|
||
</el-link>
|
||
<el-link
|
||
v-for="operation in customOperations(row)"
|
||
:key="operation.action"
|
||
:type="operation.type || 'primary'"
|
||
@click="handleCustomOperation(operation, row)"
|
||
>
|
||
{{ operation.label }}
|
||
</el-link>
|
||
<el-link type="primary" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
|
||
</template>
|
||
</avue-crud>
|
||
|
||
<empty-pagination
|
||
:page="page"
|
||
@size-change="sizeChange"
|
||
@current-change="currentChange"
|
||
@load="onLoad(page, query)"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="projectBox"
|
||
:title="projectDialogTitle"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="1440px"
|
||
top="4vh"
|
||
class="project-apply-dialog"
|
||
@closed="resetProjectDialog"
|
||
>
|
||
<el-form
|
||
ref="projectForm"
|
||
:model="form"
|
||
:rules="formRules"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="project-apply-form"
|
||
:disabled="dialogReadonly"
|
||
>
|
||
<div class="dialog-section-title">项目基本信息</div>
|
||
<div class="project-apply-form__grid">
|
||
<el-form-item v-if="isChangeDialog" label="变更类型" prop="changeType">
|
||
<el-select v-model="form.changeType" placeholder="请选择变更类型">
|
||
<el-option
|
||
v-for="item in changeTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<div v-if="isChangeDialog"></div>
|
||
<el-form-item label="项目类型" prop="projectType">
|
||
<el-select
|
||
v-model="form.projectType"
|
||
placeholder="请选择项目类型"
|
||
clearable
|
||
:disabled="isBasicInfoReadonly || isMajorProjectSupplementDialog"
|
||
@change="handleProjectTypeChange"
|
||
>
|
||
<el-option
|
||
v-for="item in projectTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目编号" prop="projectCode">
|
||
<el-input v-model="form.projectCode" disabled placeholder="系统自动生成" />
|
||
</el-form-item>
|
||
<el-form-item label="项目名称" prop="projectName">
|
||
<el-input
|
||
v-model="form.projectName"
|
||
placeholder="请填写项目名称"
|
||
maxlength="50"
|
||
:disabled="isBasicInfoReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="项目简称" prop="projectShortName">
|
||
<el-input
|
||
v-model="form.projectShortName"
|
||
placeholder="请填写项目简称"
|
||
maxlength="20"
|
||
:disabled="isBasicInfoReadonly"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="业务部门" prop="businessDeptId">
|
||
<el-select
|
||
v-model="form.businessDeptId"
|
||
placeholder="请选择业务部门"
|
||
filterable
|
||
clearable
|
||
:disabled="isBasicInfoReadonly"
|
||
@change="handleBusinessDeptChange"
|
||
>
|
||
<el-option
|
||
v-for="item in deptOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="承办部门" prop="undertakeDeptId">
|
||
<el-select
|
||
v-model="form.undertakeDeptId"
|
||
placeholder="请选择承办部门"
|
||
filterable
|
||
clearable
|
||
:disabled="isBasicInfoReadonly"
|
||
@change="handleUndertakeDeptChange"
|
||
>
|
||
<el-option
|
||
v-for="item in deptOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目由来" prop="projectSource">
|
||
<el-select
|
||
v-model="form.projectSource"
|
||
placeholder="请选择项目由来"
|
||
clearable
|
||
:disabled="isBasicInfoReadonly"
|
||
>
|
||
<el-option
|
||
v-for="item in projectSourceOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目由来说明" prop="sourceRemark">
|
||
<el-input v-model="form.sourceRemark" maxlength="300" :disabled="isBasicInfoReadonly" />
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="dialog-section-title">项目主要内容</div>
|
||
<div class="project-apply-form__grid">
|
||
<el-form-item label="客户名称" prop="customerNames">
|
||
<el-select
|
||
v-model="selectedCustomerId"
|
||
placeholder="搜索选择"
|
||
filterable
|
||
clearable
|
||
:loading="customerLoading"
|
||
@visible-change="visible => visible && loadCustomerOptions('客户')"
|
||
@change="handleCustomerChange"
|
||
>
|
||
<el-option
|
||
v-for="item in customerOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="下游承运商" prop="carrierNames">
|
||
<el-select
|
||
v-model="selectedCarrierIds"
|
||
placeholder="支持多选,选择后下方列表展示"
|
||
multiple
|
||
filterable
|
||
clearable
|
||
collapse-tags
|
||
:loading="carrierLoading"
|
||
@visible-change="visible => visible && loadCustomerOptions('承运商')"
|
||
@change="handleCarrierChange"
|
||
>
|
||
<el-option
|
||
v-for="item in carrierOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目资金使用额度(万元)" prop="fundLimit">
|
||
<el-input
|
||
v-model="form.fundLimit"
|
||
placeholder="实际业务回款周期内付款额度"
|
||
@input="value => handleNumberInput('fundLimit', value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="项目应收账款额度(万元)" prop="receivableLimit">
|
||
<el-input
|
||
v-model="form.receivableLimit"
|
||
placeholder="回款期内最大应收账款额度"
|
||
@input="value => handleNumberInput('receivableLimit', value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="应收账款回款期限" prop="receivableDays">
|
||
<el-input
|
||
v-model="form.receivableDays"
|
||
@input="value => handleIntegerInput('receivableDays', value)"
|
||
>
|
||
<template #suffix>天</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="回款账期" prop="paymentDays">
|
||
<el-input
|
||
v-model="form.paymentDays"
|
||
@input="value => handleIntegerInput('paymentDays', value)"
|
||
>
|
||
<template #suffix>天</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="货物类型" prop="cargoType">
|
||
<el-select v-model="form.cargoType" placeholder="请选择货物类型" filterable clearable>
|
||
<el-option
|
||
v-for="item in cargoTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="预估货物数量" prop="cargoQuantity">
|
||
<el-input
|
||
v-model="form.cargoQuantity"
|
||
placeholder="请输入数字"
|
||
@input="value => handleIntegerInput('cargoQuantity', value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="业务周期" prop="businessDateRange">
|
||
<el-date-picker
|
||
v-model="form.businessDateRange"
|
||
type="daterange"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
unlink-panels
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="运输线路" prop="transportRoute">
|
||
<el-input v-model="form.transportRoute" maxlength="100" />
|
||
</el-form-item>
|
||
<el-form-item label="运输类型" prop="transportType">
|
||
<el-select v-model="form.transportType" placeholder="请选择运输类型" clearable>
|
||
<el-option
|
||
v-for="item in transportTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="业务类型" prop="businessType">
|
||
<el-select v-model="form.businessType" placeholder="请选择业务类型" clearable>
|
||
<el-option
|
||
v-for="item in businessTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目规模(万元)" prop="projectScale">
|
||
<el-input
|
||
v-model="form.projectScale"
|
||
@input="value => handleNumberInput('projectScale', value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="结算方式" prop="settlementMode">
|
||
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable>
|
||
<el-option
|
||
v-for="item in settlementModeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="预估利润(万元)" prop="estimatedProfit">
|
||
<el-input
|
||
v-model="form.estimatedProfit"
|
||
@input="value => handleNumberInput('estimatedProfit', value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="资金需求(万元)" prop="fundDemand">
|
||
<el-input
|
||
v-model="form.fundDemand"
|
||
@input="value => handleNumberInput('fundDemand', value)"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="项目经办人" prop="handlerUserId">
|
||
<el-input
|
||
v-model="form.handlerUserName"
|
||
readonly
|
||
placeholder="弹窗搜索选择"
|
||
@click="openUserPicker('handler')"
|
||
>
|
||
<template #suffix>
|
||
<el-icon class="project-apply-form__picker-icon"><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="项目负责人" prop="principalUserId">
|
||
<el-input
|
||
v-model="form.principalUserName"
|
||
readonly
|
||
placeholder="弹窗搜索选择"
|
||
@click="openUserPicker('principal')"
|
||
>
|
||
<template #suffix>
|
||
<el-icon class="project-apply-form__picker-icon"><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="dialog-section-title project-apply-form__table-title">客户信息</div>
|
||
<el-table :data="customerRows" border class="project-apply-form__table">
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="customer" label="客户" min-width="160" align="center" />
|
||
<el-table-column prop="credit" label="客户信用额度" min-width="150" align="center" />
|
||
<el-table-column prop="companyNature" label="企业性质" min-width="140" align="center" />
|
||
<el-table-column prop="legalPerson" label="法定代表人" min-width="140" align="center" />
|
||
<el-table-column
|
||
prop="address"
|
||
label="联系地址"
|
||
min-width="180"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="contact" label="联系人" min-width="120" align="center" />
|
||
<el-table-column prop="phone" label="联系方式" min-width="140" align="center" />
|
||
</el-table>
|
||
|
||
<div class="dialog-section-title">承运商信息</div>
|
||
<el-table :data="carrierRows" border class="project-apply-form__table">
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="carrier" label="承运商" min-width="180" align="center" />
|
||
<el-table-column prop="companyNature" label="企业性质" min-width="140" align="center" />
|
||
<el-table-column prop="legalPerson" label="法定代表人" min-width="140" align="center" />
|
||
<el-table-column
|
||
prop="address"
|
||
label="联系地址"
|
||
min-width="180"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="contact" label="联系人" min-width="120" align="center" />
|
||
<el-table-column prop="phone" label="联系方式" min-width="140" align="center" />
|
||
</el-table>
|
||
|
||
<div class="dialog-section-title">项目情况说明</div>
|
||
<div class="project-apply-form__textarea-list">
|
||
<el-form-item label="项目简介" prop="projectIntro">
|
||
<el-input
|
||
v-model="form.projectIntro"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="盈利模式及利润测算情况说明" prop="profitRemark">
|
||
<el-input
|
||
v-model="form.profitRemark"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="项目风险点" prop="riskPoint">
|
||
<el-input
|
||
v-model="form.riskPoint"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="风险防范措施及应急预案" prop="emergencyPlan">
|
||
<el-input
|
||
v-model="form.emergencyPlan"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="project-apply-form__material-head">
|
||
<div class="dialog-section-title project-apply-form__material-title">项目材料</div>
|
||
<el-button type="primary" :disabled="!attachmentRows.length" @click="handleBatchDownload"
|
||
>批量下载</el-button
|
||
>
|
||
</div>
|
||
<el-table :data="attachmentRows" border class="project-apply-form__table">
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column label="附件类型" min-width="160" align="center">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.fileType" placeholder="请选择" :disabled="dialogReadonly">
|
||
<el-option
|
||
v-for="item in projectAttachmentTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ row.originalName || row.name }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="附件描述" min-width="240" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-model="row.description"
|
||
placeholder="请输入附件描述"
|
||
:disabled="dialogReadonly"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="文件大小" min-width="120" align="center">
|
||
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUserName" label="上传人" min-width="140" align="center" />
|
||
<el-table-column prop="uploadTime" label="上传时间" min-width="170" align="center" />
|
||
<el-table-column label="操作" width="110" align="center">
|
||
<template #default="{ row, $index }">
|
||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||
<el-link
|
||
v-if="!dialogReadonly"
|
||
type="primary"
|
||
@click="removeAttachment($index)"
|
||
>删除</el-link
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<vehicle-attachment-upload
|
||
v-model="attachmentRows"
|
||
class="project-apply-form__upload"
|
||
:readonly="dialogReadonly"
|
||
:file-types="attachmentFileTypes"
|
||
:max-size="500"
|
||
button-text="上传附件"
|
||
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件大小限制500M"
|
||
@change="handleAttachmentChange"
|
||
/>
|
||
|
||
<div v-if="!isChangeDialog" class="dialog-section-title project-apply-form__table-title">
|
||
变更记录
|
||
</div>
|
||
<el-table
|
||
v-if="!isChangeDialog"
|
||
:data="changeRows"
|
||
border
|
||
class="project-apply-form__table"
|
||
>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="changeDate" label="变更日期" min-width="140" align="center" />
|
||
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
|
||
<el-table-column prop="handler" label="经办人" min-width="120" align="center" />
|
||
<el-table-column
|
||
prop="content"
|
||
label="变更内容"
|
||
min-width="180"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="reason"
|
||
label="变更原因"
|
||
min-width="180"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column prop="status" label="状态" min-width="120" align="center" />
|
||
<el-table-column label="操作" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="handleChangeRecordAction(row)">
|
||
{{ row.action || '查看' }}
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<template v-if="isChangeDialog">
|
||
<div class="dialog-section-title">变更内容</div>
|
||
<el-form-item prop="changeContent" class="project-apply-form__change-textarea">
|
||
<el-input
|
||
v-model="form.changeContent"
|
||
type="textarea"
|
||
:rows="5"
|
||
placeholder="2000字以内"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<div class="dialog-section-title">变更原因</div>
|
||
<el-form-item prop="changeReason" class="project-apply-form__change-textarea">
|
||
<el-input
|
||
v-model="form.changeReason"
|
||
type="textarea"
|
||
:rows="5"
|
||
placeholder="2000字以内"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</template>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div
|
||
class="project-apply-dialog__footer"
|
||
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
|
||
>
|
||
<template v-if="isChangeDialog">
|
||
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
|
||
提交
|
||
</el-button>
|
||
<el-button type="primary" :loading="submitLoading" @click="saveChangeProject">
|
||
保存
|
||
</el-button>
|
||
<el-button @click="projectBox = false">关闭</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-button @click="projectBox = false">取消</el-button>
|
||
<el-button
|
||
v-if="!dialogReadonly"
|
||
type="primary"
|
||
:loading="submitLoading"
|
||
@click="submitProject"
|
||
>
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="userBox"
|
||
title="选择用户"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="900px"
|
||
class="project-apply-user-dialog"
|
||
>
|
||
<div class="project-apply-user-dialog__search">
|
||
<el-input
|
||
v-model="userQuery.realName"
|
||
placeholder="用户姓名"
|
||
clearable
|
||
@keyup.enter="loadUsers"
|
||
/>
|
||
<el-input
|
||
v-model="userQuery.account"
|
||
placeholder="登录账号"
|
||
clearable
|
||
@keyup.enter="loadUsers"
|
||
/>
|
||
<el-button type="primary" @click="loadUsers">查询</el-button>
|
||
<el-button @click="resetUserQuery">重置</el-button>
|
||
</div>
|
||
<el-table :data="userData" border v-loading="userLoading" @row-dblclick="selectUser">
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column prop="account" label="登录账号" min-width="140" align="center" />
|
||
<el-table-column prop="realName" label="用户姓名" min-width="140" align="center" />
|
||
<el-table-column prop="deptName" label="所属部门" min-width="180" align="center" />
|
||
<el-table-column label="操作" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" @click="selectUser(row)">选择</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="project-apply-user-dialog__pager">
|
||
<el-pagination
|
||
v-model:current-page="userPage.currentPage"
|
||
v-model:page-size="userPage.pageSize"
|
||
:page-sizes="[10, 20, 50]"
|
||
:total="userPage.total"
|
||
layout="total, sizes, prev, pager, next"
|
||
@current-change="loadUsers"
|
||
@size-change="loadUsers"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
</basic-container>
|
||
</template>
|
||
|
||
<script>
|
||
import { exportBlob } from '@/api/common';
|
||
import * as api from '@/api/business/project-apply';
|
||
import {
|
||
getList as getCustomerArchiveList,
|
||
getDetail as getCustomerArchiveDetail,
|
||
} from '@/api/vehicle/customer-archive';
|
||
import { getDeptTree } from '@/api/system/dept';
|
||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||
import { getList as getUserList } from '@/api/system/user';
|
||
import { getToken } from '@/utils/auth';
|
||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||
import { mapGetters } from 'vuex';
|
||
import NProgress from 'nprogress';
|
||
import 'nprogress/nprogress.css';
|
||
import {
|
||
businessTypeOptions,
|
||
projectSourceOptions,
|
||
projectTypeOptions,
|
||
settlementModeOptions,
|
||
} from '@/option/business/common';
|
||
import { config, option } from '@/option/business/project-apply';
|
||
|
||
const emptyForm = () => ({
|
||
projectType: '',
|
||
projectCode: '',
|
||
projectName: '',
|
||
projectShortName: '',
|
||
businessDeptId: '',
|
||
businessDeptName: '',
|
||
undertakeDeptId: '',
|
||
undertakeDeptName: '',
|
||
projectSource: '商务洽谈',
|
||
sourceRemark: '',
|
||
changeType: '',
|
||
customerNames: '',
|
||
carrierNames: '',
|
||
fundLimit: '',
|
||
receivableLimit: '',
|
||
receivableDays: '',
|
||
paymentDays: '',
|
||
cargoType: '',
|
||
cargoQuantity: '',
|
||
businessDateRange: [],
|
||
businessStartDate: '',
|
||
businessEndDate: '',
|
||
transportRoute: '',
|
||
transportType: '',
|
||
businessType: '',
|
||
projectScale: '',
|
||
settlementMode: '',
|
||
estimatedProfit: '',
|
||
fundDemand: '',
|
||
handlerUserId: '',
|
||
handlerUserName: '',
|
||
principalUserId: '',
|
||
principalUserName: '',
|
||
customerJson: '',
|
||
carrierJson: '',
|
||
attachmentsJson: '',
|
||
situationRemark: '',
|
||
projectIntro: '',
|
||
profitRemark: '',
|
||
riskPoint: '',
|
||
emergencyPlan: '',
|
||
changeContent: '',
|
||
changeReason: '',
|
||
});
|
||
|
||
const changeTypeOptions = [
|
||
{ label: '项目变更', value: '项目变更' },
|
||
{ label: '项目备案调整', value: '项目备案调整' },
|
||
];
|
||
|
||
const normalProjectAttachmentTypeOptions = [
|
||
'利润测算表',
|
||
'单一来源供应商申请表/三方比价表',
|
||
'合同模板',
|
||
'项目操作说明',
|
||
'项目分析报告',
|
||
'项目考察报告',
|
||
].map(item => ({ label: item, value: item }));
|
||
|
||
const majorProjectAttachmentTypeOptions = [
|
||
'会议纪要',
|
||
'评审表决票',
|
||
'利润测算表',
|
||
'单一来源供应商申请表/三方比价表',
|
||
'项目操作说明',
|
||
'项目分析报告',
|
||
'项目考察报告',
|
||
'合同模板',
|
||
'上游尽职调查报告',
|
||
'下游尽职调查报告',
|
||
'业务承诺函',
|
||
].map(item => ({ label: item, value: item }));
|
||
|
||
export default {
|
||
data() {
|
||
const validateAmount = (rule, value, callback) => {
|
||
if (value === '' || value === undefined || value === null) {
|
||
callback();
|
||
return;
|
||
}
|
||
if (Number(value) < 0) {
|
||
callback(new Error(`${rule.label || '金额'}不能小于0`));
|
||
return;
|
||
}
|
||
callback();
|
||
};
|
||
const validateRequiredAmount = (rule, value, callback) => {
|
||
if (value === '' || value === undefined || value === null) {
|
||
callback(new Error(`请输入${rule.label}`));
|
||
return;
|
||
}
|
||
validateAmount(rule, value, callback);
|
||
};
|
||
const validateDays = (rule, value, callback) => {
|
||
if (value === '' || value === undefined || value === null) {
|
||
callback(new Error(`请输入${rule.label}`));
|
||
return;
|
||
}
|
||
const number = Number(value);
|
||
if (!Number.isInteger(number) || number < 0 || number > 999) {
|
||
callback(new Error(`${rule.label}范围为0到999天`));
|
||
return;
|
||
}
|
||
callback();
|
||
};
|
||
const validateUser = (rule, value, callback) => {
|
||
if (value || this.form[rule.nameProp]) {
|
||
callback();
|
||
return;
|
||
}
|
||
callback(new Error(`请选择${rule.label}`));
|
||
};
|
||
return {
|
||
api,
|
||
config,
|
||
projectTypeOptions,
|
||
projectSourceOptions,
|
||
transportTypeOptions: [],
|
||
businessTypeOptions,
|
||
settlementModeOptions,
|
||
changeTypeOptions,
|
||
tableForm: {},
|
||
tableOption: this.buildTableOption(),
|
||
loading: true,
|
||
data: [],
|
||
query: {},
|
||
allDept: 0,
|
||
page: {
|
||
pageSize: 10,
|
||
pageSizes: [10, 20, 50, 100],
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
selectionList: [],
|
||
projectBox: false,
|
||
dialogType: 'add',
|
||
dialogReadonly: false,
|
||
submitLoading: false,
|
||
form: emptyForm(),
|
||
formRules: {
|
||
changeType: [{ required: true, message: '请选择变更类型', trigger: 'change' }],
|
||
projectType: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
|
||
projectName: [
|
||
{ required: true, message: '请输入项目名称', trigger: 'blur' },
|
||
{ max: 50, message: '项目名称不能超过50个字符', trigger: 'blur' },
|
||
],
|
||
projectShortName: [
|
||
{ required: true, message: '请输入项目简称', trigger: 'blur' },
|
||
{ max: 20, message: '项目简称不能超过20个字符', trigger: 'blur' },
|
||
],
|
||
businessDeptId: [{ required: true, message: '请选择业务部门', trigger: 'change' }],
|
||
undertakeDeptId: [{ required: true, message: '请选择承办部门', trigger: 'change' }],
|
||
customerNames: [{ required: true, message: '请选择客户名称', trigger: 'change' }],
|
||
carrierNames: [{ required: true, message: '请选择下游承运商', trigger: 'change' }],
|
||
fundLimit: [
|
||
{
|
||
required: true,
|
||
label: '项目资金使用额度',
|
||
validator: validateRequiredAmount,
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
receivableLimit: [
|
||
{
|
||
required: true,
|
||
label: '项目应收账款额度',
|
||
validator: validateRequiredAmount,
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
receivableDays: [
|
||
{ required: true, label: '应收账款回款期限', validator: validateDays, trigger: 'blur' },
|
||
],
|
||
paymentDays: [
|
||
{ required: true, label: '回款账期', validator: validateDays, trigger: 'blur' },
|
||
],
|
||
projectScale: [{ label: '项目规模', validator: validateAmount, trigger: 'blur' }],
|
||
estimatedProfit: [{ label: '预估利润', validator: validateAmount, trigger: 'blur' }],
|
||
fundDemand: [{ label: '资金需求', validator: validateAmount, trigger: 'blur' }],
|
||
handlerUserId: [
|
||
{
|
||
label: '项目经办人',
|
||
nameProp: 'handlerUserName',
|
||
validator: validateUser,
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
principalUserId: [
|
||
{
|
||
label: '项目负责人',
|
||
nameProp: 'principalUserName',
|
||
validator: validateUser,
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
changeContent: [
|
||
{ required: true, message: '请输入变更内容', trigger: 'blur' },
|
||
{ max: 2000, message: '变更内容不能超过2000个字符', trigger: 'blur' },
|
||
],
|
||
changeReason: [
|
||
{ required: true, message: '请输入变更原因', trigger: 'blur' },
|
||
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
|
||
],
|
||
},
|
||
deptOptions: [],
|
||
cargoTypeOptions: [],
|
||
selectedCustomerId: '',
|
||
selectedCarrierIds: [],
|
||
customerOptions: [],
|
||
carrierOptions: [],
|
||
customerLoading: false,
|
||
carrierLoading: false,
|
||
customerRows: [],
|
||
carrierRows: [],
|
||
attachmentRows: [],
|
||
attachmentFileTypes: [
|
||
'pdf',
|
||
'bmp',
|
||
'jpeg',
|
||
'png',
|
||
'jpg',
|
||
'doc',
|
||
'docx',
|
||
'ppt',
|
||
'pptx',
|
||
'xlsx',
|
||
'xls',
|
||
'eml',
|
||
'msg',
|
||
'zip',
|
||
],
|
||
changeRows: [],
|
||
userBox: false,
|
||
userPickType: '',
|
||
userLoading: false,
|
||
userData: [],
|
||
userQuery: {
|
||
realName: '',
|
||
account: '',
|
||
},
|
||
userPage: {
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
},
|
||
};
|
||
},
|
||
computed: {
|
||
...mapGetters(['permission', 'userInfo']),
|
||
permissionList() {
|
||
return {
|
||
addBtn: this.canCreate,
|
||
};
|
||
},
|
||
isAdmin() {
|
||
const authority = this.userInfo && this.userInfo.authority;
|
||
return Array.isArray(authority)
|
||
? authority.includes('admin')
|
||
: String(authority || '').includes('admin');
|
||
},
|
||
canViewAllDept() {
|
||
return this.isAdmin;
|
||
},
|
||
readonlyScope() {
|
||
return this.config.enableAllDept && this.allDept === 1;
|
||
},
|
||
canCreate() {
|
||
return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
|
||
},
|
||
canMajorProjectSupplement() {
|
||
return this.hasPermission(`${this.config.permission}_supplement`) && !this.readonlyScope;
|
||
},
|
||
isMajorProjectSupplementDialog() {
|
||
return this.dialogType === 'majorSupplement';
|
||
},
|
||
isChangeDialog() {
|
||
return this.dialogType === 'change';
|
||
},
|
||
isBasicInfoReadonly() {
|
||
return this.dialogReadonly || this.isChangeDialog;
|
||
},
|
||
ids() {
|
||
return this.selectionList.map(item => item.id).join(',');
|
||
},
|
||
projectDialogTitle() {
|
||
if (this.isChangeDialog) {
|
||
return '项目变更';
|
||
}
|
||
if (this.isMajorProjectSupplementDialog) {
|
||
return '重大项目补录';
|
||
}
|
||
const text = { add: '新增', edit: '编辑', view: '查看' }[this.dialogType] || '新增';
|
||
return `${text}${this.config.title}`;
|
||
},
|
||
projectAttachmentTypeOptions() {
|
||
return this.form.projectType === '重大项目'
|
||
? majorProjectAttachmentTypeOptions
|
||
: normalProjectAttachmentTypeOptions;
|
||
},
|
||
},
|
||
created() {
|
||
this.loadDeptOptions();
|
||
this.loadCargoTypeOptions();
|
||
this.loadTransportTypeOptions();
|
||
},
|
||
methods: {
|
||
buildTableOption() {
|
||
return applyTableMenuWidth(
|
||
{
|
||
...option,
|
||
dialogType: 'dialog',
|
||
addBtn: false,
|
||
viewBtn: false,
|
||
editBtn: false,
|
||
delBtn: false,
|
||
column: (option.column || []).map(column => ({ ...column, display: false })),
|
||
},
|
||
4
|
||
);
|
||
},
|
||
hasPermission(code) {
|
||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||
},
|
||
statusValue(row) {
|
||
return row[this.config.statusProp || 'status'];
|
||
},
|
||
inStatus(row, statusList) {
|
||
if (!statusList || !statusList.length) return true;
|
||
return statusList.includes(this.statusValue(row));
|
||
},
|
||
canEdit(row) {
|
||
return (
|
||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||
!this.readonlyScope &&
|
||
!row.readonly &&
|
||
this.inStatus(row, this.config.editStatus)
|
||
);
|
||
},
|
||
canDelete(row) {
|
||
return (
|
||
this.hasPermission(`${this.config.permission}_delete`) &&
|
||
!this.readonlyScope &&
|
||
!row.readonly &&
|
||
this.inStatus(row, this.config.deleteStatus)
|
||
);
|
||
},
|
||
customOperations(row) {
|
||
return (this.config.operations || []).filter(operation => this.canOperation(operation, row));
|
||
},
|
||
canOperation(operation, row) {
|
||
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
|
||
if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) return false;
|
||
const prop = operation.statusProp || this.config.statusProp || 'status';
|
||
const statusList = operation.status || [];
|
||
return !statusList.length || statusList.includes(row[prop]);
|
||
},
|
||
displayStatus(row, prop) {
|
||
const textProp = prop === this.config.statusProp ? this.config.statusTextProp : '';
|
||
if (textProp && row[textProp]) return row[textProp];
|
||
const column = this.findColumn(this.tableOption.column, prop);
|
||
const item = column && (column.dicData || []).find(dic => dic.value === row[prop]);
|
||
return item ? item.label : row[prop] || '未知';
|
||
},
|
||
statusTagType(status) {
|
||
if (
|
||
[
|
||
1,
|
||
'pending',
|
||
'waiting_dispatch',
|
||
'processing',
|
||
'dispatching',
|
||
'approved',
|
||
'change_approved',
|
||
].includes(status)
|
||
) {
|
||
return 'success';
|
||
}
|
||
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) return 'info';
|
||
if (['rejected', 'change_rejected'].includes(status)) return 'danger';
|
||
if (status === 'completed') return 'primary';
|
||
return 'warning';
|
||
},
|
||
onLoad(page, params = {}) {
|
||
this.loading = true;
|
||
this.api
|
||
.getList(page.currentPage, page.pageSize, {
|
||
...params,
|
||
...this.query,
|
||
allDept: this.allDept,
|
||
})
|
||
.then(res => {
|
||
const result = res.data.data || {};
|
||
this.page.total = result.total || 0;
|
||
this.data = result.records || [];
|
||
this.selectionClear();
|
||
})
|
||
.finally(() => {
|
||
this.loading = false;
|
||
});
|
||
},
|
||
searchReset() {
|
||
this.query = {};
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
},
|
||
searchChange(params, done) {
|
||
this.query = params;
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page, params);
|
||
done();
|
||
},
|
||
selectionChange(list) {
|
||
this.selectionList = list;
|
||
},
|
||
selectionClear() {
|
||
this.selectionList = [];
|
||
if (this.$refs.crud) {
|
||
this.$refs.crud.toggleSelection();
|
||
}
|
||
},
|
||
currentChange(currentPage) {
|
||
this.page.currentPage = currentPage;
|
||
},
|
||
sizeChange(pageSize) {
|
||
this.page.pageSize = pageSize;
|
||
},
|
||
refreshChange() {
|
||
this.onLoad(this.page, this.query);
|
||
},
|
||
handleScopeChange() {
|
||
if (!this.canViewAllDept) this.allDept = 0;
|
||
this.page.currentPage = 1;
|
||
this.onLoad(this.page);
|
||
},
|
||
rowDel(row) {
|
||
this.$confirm('确定将选择数据删除?', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => this.api.remove(row.id))
|
||
.then(res => this.afterRemove(res.data.data));
|
||
},
|
||
handleDelete() {
|
||
if (!this.selectionList.length) {
|
||
this.$message.warning('请选择至少一条数据');
|
||
return;
|
||
}
|
||
const selection = this.selectionList.filter(item => this.canDelete(item));
|
||
if (!selection.length) {
|
||
this.$message.warning('所选数据当前状态不可删除');
|
||
return;
|
||
}
|
||
this.$confirm('确定将选择数据删除?', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
})
|
||
.then(() => this.api.remove(selection.map(item => item.id).join(',')))
|
||
.then(res => this.afterRemove(res.data.data));
|
||
},
|
||
afterRemove(result = {}) {
|
||
this.onLoad(this.page);
|
||
const successCount = result.successCount || 0;
|
||
const skippedCount = result.skippedCount || 0;
|
||
if (skippedCount) {
|
||
this.$message.warning(`成功删除${successCount}条,因业务引用跳过${skippedCount}条`);
|
||
} else {
|
||
this.$message.success('删除成功!');
|
||
}
|
||
this.selectionClear();
|
||
},
|
||
handleCustomOperation(operation, row) {
|
||
if (operation.action === 'startChange') {
|
||
this.openProjectDialog('change', row);
|
||
return;
|
||
}
|
||
const run = value => {
|
||
const args = operation.prompt ? [row.id, value] : [row.id];
|
||
this.api[operation.action](...args).then(res => {
|
||
if (res.data?.success === false || res.data?.data === false) {
|
||
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
|
||
return;
|
||
}
|
||
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: operation.promptError || '请输入内容',
|
||
}).then(({ value }) => run(value));
|
||
return;
|
||
}
|
||
this.$confirm(operation.confirm || `确定${operation.label}该${this.config.title}?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => run());
|
||
},
|
||
buildExportParams() {
|
||
return {
|
||
...this.query,
|
||
allDept: this.allDept,
|
||
ids: this.ids,
|
||
[this.website.tokenHeader]: getToken(),
|
||
};
|
||
},
|
||
handleExport() {
|
||
this.$confirm(`是否导出${this.config.exportName || this.config.title}?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => {
|
||
NProgress.start();
|
||
exportBlob(this.config.exportUrl, this.buildExportParams(), { feedback: true })
|
||
.then(res => {
|
||
downloadXls(
|
||
res.data,
|
||
`${this.config.exportName || this.config.title}${this.$dayjs().format(
|
||
'YYYY-MM-DD HH:mm:ss'
|
||
)}.xlsx`
|
||
);
|
||
})
|
||
.finally(() => {
|
||
NProgress.done();
|
||
});
|
||
});
|
||
},
|
||
openProjectDialog(type, row = {}) {
|
||
this.dialogType = type;
|
||
this.dialogReadonly = type === 'view';
|
||
this.projectBox = true;
|
||
if (type === 'add' || type === 'majorSupplement') {
|
||
this.applyProjectDetail({
|
||
...emptyForm(),
|
||
projectType: type === 'majorSupplement' ? '重大项目' : '',
|
||
});
|
||
this.fillDefaultUsers();
|
||
return;
|
||
}
|
||
this.api.getDetail(row.id).then(res => {
|
||
const detail = res.data.data || {};
|
||
this.applyProjectDetail({
|
||
...detail,
|
||
changeType: type === 'change' ? this.resolveChangeType(detail) : detail.changeType,
|
||
});
|
||
});
|
||
},
|
||
resetProjectDialog() {
|
||
this.$refs.projectForm?.clearValidate();
|
||
this.form = emptyForm();
|
||
this.customerRows = [];
|
||
this.carrierRows = [];
|
||
this.attachmentRows = [];
|
||
this.changeRows = [];
|
||
this.selectedCustomerId = '';
|
||
this.selectedCarrierIds = [];
|
||
},
|
||
applyProjectDetail(row) {
|
||
const form = {
|
||
...emptyForm(),
|
||
...row,
|
||
businessDateRange:
|
||
row.businessStartDate && row.businessEndDate
|
||
? [row.businessStartDate, row.businessEndDate]
|
||
: [],
|
||
};
|
||
const situation = this.parseSituation(row.situationRemark);
|
||
const customerRows = this.parseJsonArray(row.customerJson);
|
||
const carrierRows = this.parseJsonArray(row.carrierJson);
|
||
this.form = {
|
||
...form,
|
||
...situation,
|
||
customerNames: this.getCustomerRowNames(customerRows, 'customer') || form.customerNames,
|
||
carrierNames: this.getCustomerRowNames(carrierRows, 'carrier') || form.carrierNames,
|
||
};
|
||
this.customerRows = customerRows;
|
||
this.carrierRows = carrierRows;
|
||
this.attachmentRows = this.parseJsonArray(row.attachmentsJson);
|
||
this.changeRows = this.buildChangeRows(row);
|
||
this.selectedCustomerId = this.getCustomerRowId(this.customerRows[0]);
|
||
this.selectedCarrierIds = this.carrierRows
|
||
.map(item => this.getCustomerRowId(item))
|
||
.filter(Boolean);
|
||
this.syncSelectedCustomerOptions();
|
||
},
|
||
fillDefaultUsers() {
|
||
const userId = this.userInfo?.userId || this.userInfo?.id || '';
|
||
const userName =
|
||
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
|
||
if (userId && userName) {
|
||
this.form.handlerUserId = userId;
|
||
this.form.handlerUserName = userName;
|
||
this.form.principalUserId = userId;
|
||
this.form.principalUserName = userName;
|
||
}
|
||
},
|
||
resolveChangeType(row = {}) {
|
||
if (row.changeType) return row.changeType;
|
||
const node = row.currentNode || '';
|
||
if (String(node).includes('备案')) return '项目备案调整';
|
||
return '项目变更';
|
||
},
|
||
submitProject() {
|
||
this.$refs.projectForm.validate(valid => {
|
||
if (!valid) {
|
||
this.handleValidateFail();
|
||
return;
|
||
}
|
||
if (!this.validateAttachmentFileTypes()) return;
|
||
this.submitLoading = true;
|
||
this.api
|
||
.submit(this.normalizeSubmitForm())
|
||
.then(
|
||
res => {
|
||
if (res.data?.success === false || res.data?.data === false) {
|
||
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
|
||
return;
|
||
}
|
||
this.$message.success('操作成功!');
|
||
this.projectBox = false;
|
||
this.onLoad(this.page, this.query);
|
||
},
|
||
error => {
|
||
window.console.log(error);
|
||
}
|
||
)
|
||
.finally(() => {
|
||
this.submitLoading = false;
|
||
});
|
||
});
|
||
},
|
||
saveChangeProject() {
|
||
this.submitChangeForm(false);
|
||
},
|
||
submitChangeProject() {
|
||
this.submitChangeForm(true);
|
||
},
|
||
submitChangeForm(needSubmit) {
|
||
this.$refs.projectForm.validate(valid => {
|
||
if (!valid) {
|
||
this.handleValidateFail();
|
||
return;
|
||
}
|
||
if (!this.validateAttachmentFileTypes()) return;
|
||
this.submitLoading = true;
|
||
const request = needSubmit ? this.api.submitChange : this.api.saveChange;
|
||
request(this.normalizeSubmitForm({ includeChangeType: true }))
|
||
.then(res => {
|
||
if (res.data?.success === false || res.data?.data === false) {
|
||
this.$message.error(
|
||
res.data?.msg || `${needSubmit ? '提交' : '保存'}失败,请联系管理员`
|
||
);
|
||
return;
|
||
}
|
||
this.$message.success(`${needSubmit ? '提交' : '保存'}成功!`);
|
||
this.projectBox = false;
|
||
this.onLoad(this.page, this.query);
|
||
})
|
||
.finally(() => {
|
||
this.submitLoading = false;
|
||
});
|
||
});
|
||
},
|
||
normalizeSubmitForm(options = {}) {
|
||
const [businessStartDate, businessEndDate] = this.form.businessDateRange || [];
|
||
const submitRow = {
|
||
...this.form,
|
||
businessStartDate,
|
||
businessEndDate,
|
||
customerJson: JSON.stringify(this.customerRows),
|
||
carrierJson: JSON.stringify(this.carrierRows),
|
||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||
situationRemark: JSON.stringify({
|
||
projectIntro: this.form.projectIntro || '',
|
||
profitRemark: this.form.profitRemark || '',
|
||
riskPoint: this.form.riskPoint || '',
|
||
emergencyPlan: this.form.emergencyPlan || '',
|
||
}),
|
||
};
|
||
delete submitRow.businessDateRange;
|
||
if (!options.includeChangeType) {
|
||
delete submitRow.changeType;
|
||
}
|
||
['projectIntro', 'profitRemark', 'riskPoint', 'emergencyPlan'].forEach(
|
||
key => delete submitRow[key]
|
||
);
|
||
Object.keys(submitRow).forEach(key => {
|
||
if (typeof submitRow[key] === 'string') {
|
||
submitRow[key] = submitRow[key].trim();
|
||
}
|
||
});
|
||
return submitRow;
|
||
},
|
||
normalizeAttachmentFileTypes() {
|
||
const values = this.projectAttachmentTypeOptions.map(item => item.value);
|
||
this.attachmentRows = this.attachmentRows.map(row => ({
|
||
...row,
|
||
fileType: values.includes(row.fileType) ? row.fileType : '',
|
||
}));
|
||
},
|
||
validateAttachmentFileTypes() {
|
||
const values = this.projectAttachmentTypeOptions.map(item => item.value);
|
||
const invalidIndex = this.attachmentRows.findIndex(row => !values.includes(row.fileType));
|
||
if (invalidIndex === -1) return true;
|
||
this.$message.warning(`请选择第${invalidIndex + 1}行项目材料的附件类型`);
|
||
this.$nextTick(() => {
|
||
const field = this.$el.querySelector('.project-apply-form__material-head');
|
||
if (field && typeof field.scrollIntoView === 'function') {
|
||
field.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
});
|
||
return false;
|
||
},
|
||
handleValidateFail() {
|
||
this.$message.warning('请完善表单必填项后再提交');
|
||
this.$nextTick(() => {
|
||
const field = this.$el.querySelector('.is-error');
|
||
if (field && typeof field.scrollIntoView === 'function') {
|
||
field.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
});
|
||
},
|
||
parseSituation(value) {
|
||
if (!value) {
|
||
return { projectIntro: '', profitRemark: '', riskPoint: '', emergencyPlan: '' };
|
||
}
|
||
try {
|
||
const data = JSON.parse(value);
|
||
if (data && typeof data === 'object') {
|
||
return {
|
||
projectIntro: data.projectIntro || '',
|
||
profitRemark: data.profitRemark || '',
|
||
riskPoint: data.riskPoint || '',
|
||
emergencyPlan: data.emergencyPlan || '',
|
||
};
|
||
}
|
||
} catch (error) {
|
||
return { projectIntro: value, profitRemark: '', riskPoint: '', emergencyPlan: '' };
|
||
}
|
||
return { projectIntro: String(value), profitRemark: '', riskPoint: '', emergencyPlan: '' };
|
||
},
|
||
parseJsonArray(value) {
|
||
if (!value) return [];
|
||
if (Array.isArray(value)) return value;
|
||
try {
|
||
const list = JSON.parse(value);
|
||
return Array.isArray(list) ? list : [];
|
||
} catch (error) {
|
||
return [];
|
||
}
|
||
},
|
||
getCustomerRowId(row = {}) {
|
||
return row.id || row.customerId || row.carrierId || '';
|
||
},
|
||
getCustomerRowLabel(row = {}, type) {
|
||
return (
|
||
row[type === 'carrier' ? 'carrier' : 'customer'] ||
|
||
row.fullName ||
|
||
row.shortName ||
|
||
row.customerName ||
|
||
row.carrierName ||
|
||
row.customerCode ||
|
||
''
|
||
);
|
||
},
|
||
getCustomerRowNames(rows = [], type) {
|
||
return rows
|
||
.map(row => this.getCustomerRowLabel(row, type))
|
||
.filter(Boolean)
|
||
.join('、');
|
||
},
|
||
buildCustomerOptionsFromRows(rows = [], type) {
|
||
return rows
|
||
.map(row => {
|
||
const value = this.getCustomerRowId(row);
|
||
const label = this.getCustomerRowLabel(row, type);
|
||
return value && label ? { label, value, raw: row } : null;
|
||
})
|
||
.filter(Boolean);
|
||
},
|
||
mergeCustomerOptions(options = [], selectedOptions = []) {
|
||
const result = [...selectedOptions];
|
||
options.forEach(option => {
|
||
const exists = result.some(item => String(item.value) === String(option.value));
|
||
if (!exists) {
|
||
result.push(option);
|
||
}
|
||
});
|
||
return result;
|
||
},
|
||
syncSelectedCustomerOptions() {
|
||
this.customerOptions = this.mergeCustomerOptions(
|
||
this.customerOptions,
|
||
this.buildCustomerOptionsFromRows(this.customerRows, 'customer')
|
||
);
|
||
this.carrierOptions = this.mergeCustomerOptions(
|
||
this.carrierOptions,
|
||
this.buildCustomerOptionsFromRows(this.carrierRows, 'carrier')
|
||
);
|
||
},
|
||
buildChangeRows(row) {
|
||
if (!row || (!row.changeContent && !row.changeReason)) return [];
|
||
return [
|
||
{
|
||
changeDate: row.updateTime || row.createTime || '',
|
||
changeType:
|
||
row.changeType ||
|
||
(String(row.currentNode || '').includes('备案') ? '项目备案调整' : '项目变更'),
|
||
handler: row.handlerUserName || '',
|
||
content: row.changeContent || '',
|
||
reason: row.changeReason || '',
|
||
status: row.approvalStatusName || row.approvalStatus || '',
|
||
action: this.dialogReadonly ? '流程' : '编辑',
|
||
},
|
||
];
|
||
},
|
||
handleBusinessDeptChange(value) {
|
||
const dept = this.deptOptions.find(item => item.value === value);
|
||
this.form.businessDeptName = dept ? dept.rawLabel : '';
|
||
this.$refs.projectForm?.validateField('businessDeptId');
|
||
},
|
||
handleUndertakeDeptChange(value) {
|
||
const dept = this.deptOptions.find(item => item.value === value);
|
||
this.form.undertakeDeptName = dept ? dept.rawLabel : '';
|
||
this.$refs.projectForm?.validateField('undertakeDeptId');
|
||
},
|
||
handleProjectTypeChange() {
|
||
this.normalizeAttachmentFileTypes();
|
||
this.$refs.projectForm?.validateField('projectType');
|
||
},
|
||
loadDeptOptions() {
|
||
getDeptTree().then(res => {
|
||
this.deptOptions = this.flattenDept(res.data.data || []);
|
||
});
|
||
},
|
||
flattenDept(list = [], level = 0) {
|
||
return list.reduce((result, item) => {
|
||
const label = item.title || item.deptName || item.name;
|
||
result.push({
|
||
label: `${' '.repeat(level)}${label}`,
|
||
rawLabel: label,
|
||
value: item.id,
|
||
});
|
||
if (item.children && item.children.length) {
|
||
result.push(...this.flattenDept(item.children, level + 1));
|
||
}
|
||
return result;
|
||
}, []);
|
||
},
|
||
loadCargoTypeOptions() {
|
||
getSystemDictionary({ code: 'type_of_goods' }).then(res => {
|
||
this.cargoTypeOptions = (res.data.data || []).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
});
|
||
},
|
||
loadTransportTypeOptions() {
|
||
getBizDictionary({ code: 'transport_type' }).then(res => {
|
||
this.transportTypeOptions = (res.data.data || []).map(item => ({
|
||
label: item.dictValue,
|
||
value: item.dictKey,
|
||
}));
|
||
});
|
||
},
|
||
loadCustomerOptions(type) {
|
||
const loadingKey = type === '客户' ? 'customerLoading' : 'carrierLoading';
|
||
const optionsKey = type === '客户' ? 'customerOptions' : 'carrierOptions';
|
||
this[loadingKey] = true;
|
||
getCustomerArchiveList(1, 100, {
|
||
customerType: type,
|
||
status: 1,
|
||
...(type === '客户' ? { approvalStatus: 'approved' } : {}),
|
||
})
|
||
.then(res => {
|
||
const records = (res.data.data && res.data.data.records) || [];
|
||
const selectedOptions = this.buildCustomerOptionsFromRows(
|
||
type === '客户' ? this.customerRows : this.carrierRows,
|
||
type === '客户' ? 'customer' : 'carrier'
|
||
);
|
||
this[optionsKey] = this.mergeCustomerOptions(
|
||
records.map(item => this.mapCustomerOption(item)),
|
||
selectedOptions
|
||
);
|
||
})
|
||
.finally(() => {
|
||
this[loadingKey] = false;
|
||
});
|
||
},
|
||
mapCustomerOption(item) {
|
||
const label = item.fullName || item.shortName || item.customerCode;
|
||
return {
|
||
label,
|
||
value: item.id,
|
||
raw: item,
|
||
};
|
||
},
|
||
handleCustomerChange(id) {
|
||
if (!id) {
|
||
this.customerRows = [];
|
||
this.form.customerNames = '';
|
||
this.form.customerJson = '';
|
||
return;
|
||
}
|
||
this.loadCustomerDetailRow(id, 'customer').then(row => {
|
||
this.customerRows = [row];
|
||
this.form.customerNames = row.customer;
|
||
this.form.customerJson = JSON.stringify(this.customerRows);
|
||
this.syncSelectedCustomerOptions();
|
||
this.$refs.projectForm?.validateField('customerNames');
|
||
});
|
||
},
|
||
handleCarrierChange(ids) {
|
||
if (!ids || !ids.length) {
|
||
this.carrierRows = [];
|
||
this.form.carrierNames = '';
|
||
this.form.carrierJson = '';
|
||
return;
|
||
}
|
||
Promise.all(ids.map(id => this.loadCustomerDetailRow(id, 'carrier'))).then(rows => {
|
||
this.carrierRows = rows;
|
||
this.form.carrierNames = rows.map(item => item.carrier).join('、');
|
||
this.form.carrierJson = JSON.stringify(this.carrierRows);
|
||
this.syncSelectedCustomerOptions();
|
||
this.$refs.projectForm?.validateField('carrierNames');
|
||
});
|
||
},
|
||
loadCustomerDetailRow(id, type) {
|
||
return getCustomerArchiveDetail(id).then(res => {
|
||
const detail = res.data.data || {};
|
||
return this.mapCustomerTableRow(detail, type);
|
||
});
|
||
},
|
||
mapCustomerTableRow(item, type) {
|
||
const contacts = item.contacts || [];
|
||
const defaultContact = contacts.find(contact => contact.isDefault === 1) || contacts[0] || {};
|
||
const name = item.fullName || item.shortName || item.customerCode || '';
|
||
const row = {
|
||
id: item.id,
|
||
credit: item.maxCreditLimit || item.applyCreditLimit || '',
|
||
companyNature: item.customerNature || '',
|
||
legalPerson: item.legalPerson || '',
|
||
address: item.registeredAddress || '',
|
||
contact: defaultContact.contactName || item.principal || '',
|
||
phone: defaultContact.contactPhone || item.contactPhone || '',
|
||
};
|
||
if (type === 'carrier') {
|
||
row.carrier = name;
|
||
} else {
|
||
row.customer = name;
|
||
}
|
||
return row;
|
||
},
|
||
handleNumberInput(prop, value) {
|
||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||
const parts = text.split('.');
|
||
this.form[prop] =
|
||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||
},
|
||
handleIntegerInput(prop, value) {
|
||
this.form[prop] = String(value || '').replace(/[^\d]/g, '');
|
||
},
|
||
handleAttachmentChange(list) {
|
||
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
const values = this.projectAttachmentTypeOptions.map(item => item.value);
|
||
this.attachmentRows = (list || []).map(item => ({
|
||
...item,
|
||
fileType: values.includes(item.fileType) ? item.fileType : '',
|
||
description: item.description || '',
|
||
uploadUserName: item.uploadUserName || userName,
|
||
uploadTime: item.uploadTime || uploadTime,
|
||
}));
|
||
},
|
||
removeAttachment(index) {
|
||
this.attachmentRows.splice(index, 1);
|
||
},
|
||
formatFileSize(size) {
|
||
const number = Number(size);
|
||
if (!number) return '';
|
||
if (number < 1024) return `${number}B`;
|
||
if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
|
||
return `${(number / 1024 / 1024).toFixed(1)}MB`;
|
||
},
|
||
downloadAttachment(row) {
|
||
const url = row.url || row.link;
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空');
|
||
return;
|
||
}
|
||
downloadFileByUrl(url, row.originalName || row.name || '附件');
|
||
},
|
||
handleBatchDownload() {
|
||
this.attachmentRows.forEach(row => this.downloadAttachment(row));
|
||
},
|
||
handleChangeRecordAction() {
|
||
this.$message.info('变更记录详情由后端明细能力生成后展示');
|
||
},
|
||
openUserPicker(type) {
|
||
if (this.dialogReadonly) return;
|
||
this.userPickType = type;
|
||
this.userBox = true;
|
||
this.loadUsers();
|
||
},
|
||
loadUsers() {
|
||
this.userLoading = true;
|
||
getUserList(this.userPage.currentPage, this.userPage.pageSize, this.userQuery)
|
||
.then(res => {
|
||
const result = res.data.data || {};
|
||
this.userData = result.records || [];
|
||
this.userPage.total = result.total || 0;
|
||
})
|
||
.finally(() => {
|
||
this.userLoading = false;
|
||
});
|
||
},
|
||
resetUserQuery() {
|
||
this.userQuery = { realName: '', account: '' };
|
||
this.userPage.currentPage = 1;
|
||
this.loadUsers();
|
||
},
|
||
selectUser(row) {
|
||
if (this.userPickType === 'handler') {
|
||
this.form.handlerUserId = row.id;
|
||
this.form.handlerUserName = row.realName || row.name || row.account;
|
||
this.$refs.projectForm?.validateField('handlerUserId');
|
||
} else {
|
||
this.form.principalUserId = row.id;
|
||
this.form.principalUserName = row.realName || row.name || row.account;
|
||
this.$refs.projectForm?.validateField('principalUserId');
|
||
}
|
||
this.userBox = false;
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.project-apply-form {
|
||
padding: 0 8px;
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-date-editor) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
:deep(.el-form-item__label) {
|
||
min-width: 160px;
|
||
white-space: normal;
|
||
line-height: 20px;
|
||
}
|
||
|
||
&__grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
column-gap: 72px;
|
||
row-gap: 0;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
&__picker-icon {
|
||
color: #a8abb2;
|
||
cursor: pointer;
|
||
}
|
||
|
||
&__table-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
}
|
||
|
||
&__tip {
|
||
color: #ff7a95;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&__table {
|
||
width: 100%;
|
||
margin-bottom: 24px;
|
||
|
||
:deep(.el-table__header th) {
|
||
background: #f5f7fa;
|
||
color: #303133;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
}
|
||
|
||
&__textarea-list {
|
||
:deep(.el-form-item) {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
:deep(.el-form-item__content) {
|
||
width: calc(100% - 180px);
|
||
}
|
||
}
|
||
|
||
&__material-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
&__material-title {
|
||
flex: 1;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
&__material-warn {
|
||
margin-left: 24px;
|
||
color: #f56c6c;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&__upload {
|
||
margin: -12px 0 24px;
|
||
}
|
||
|
||
&__change-textarea {
|
||
margin-bottom: 24px;
|
||
|
||
:deep(.el-form-item__content) {
|
||
width: 100%;
|
||
margin-left: 0 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:global(.project-apply-dialog .el-dialog__body) {
|
||
max-height: 76vh;
|
||
overflow-y: auto;
|
||
padding: 16px 24px 8px;
|
||
}
|
||
|
||
.project-apply-dialog__footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
|
||
&--change {
|
||
justify-content: flex-start;
|
||
padding-left: 24px;
|
||
}
|
||
}
|
||
|
||
.project-apply-user-dialog {
|
||
&__search {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr auto auto;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
&__pager {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.project-apply-form {
|
||
&__grid {
|
||
grid-template-columns: 1fr;
|
||
column-gap: 0;
|
||
}
|
||
}
|
||
}
|
||
</style>
|