704c3093d7
- 项目编号放开编辑,新增时可输入,编辑时禁用 - 取消项目简称的必填校验,仅限制最大长度20 - 业务部门、项目由来说明表单项占满一行,优化布局 - 业务部门下拉增加过滤,重大项目补录模式不显示外部组织 - 新增变更编号及变更日期字段,关联变更类型显示 - 搜索栏将项目负责人由下拉切换为手动输入模糊查询 - 新增项目编号、客户名称、下游承运商三个搜索字段 - 清理无用的搜索工具函数和配置,优化代码结构
2405 lines
80 KiB
Vue
2405 lines
80 KiB
Vue
<template>
|
||
<basic-container class="project-apply-page">
|
||
<avue-crud
|
||
v-if="!isProjectFormPage"
|
||
: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="canCreate"
|
||
@click="openProjectDialog('add')"
|
||
>
|
||
新增
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-plus"
|
||
v-if="canMajorProjectSupplement"
|
||
@click="openProjectDialog('majorSupplement')"
|
||
>
|
||
重大项目补录
|
||
</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 }">
|
||
{{ displayStatus(row, 'approvalStatus') }}
|
||
</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="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
|
||
</template>
|
||
</avue-crud>
|
||
|
||
<empty-pagination
|
||
v-if="!isProjectFormPage"
|
||
:page="page"
|
||
@size-change="sizeChange"
|
||
@current-change="currentChange"
|
||
@load="onLoad(page, query)"
|
||
/>
|
||
|
||
<component
|
||
:is="projectFormContainer"
|
||
v-if="projectBox"
|
||
v-bind="projectFormContainerProps"
|
||
@update:model-value="projectBox = $event"
|
||
@closed="resetProjectDialog"
|
||
>
|
||
<div v-if="isProjectFormPage" class="archive-page-form__title">
|
||
{{ projectDialogTitle }}
|
||
</div>
|
||
<el-form
|
||
ref="projectForm"
|
||
:model="form"
|
||
:rules="formRules"
|
||
label-position="right"
|
||
label-width="150"
|
||
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>
|
||
<el-form-item v-if="isChangeDialog" label="变更编号" prop="changeNo">
|
||
<el-input v-model="form.changeNo" disabled placeholder="系统自动生成" />
|
||
</el-form-item>
|
||
<el-form-item v-if="isChangeDialog" label="变更日期" prop="changeDate">
|
||
<el-input v-model="form.changeDate" disabled placeholder="系统自动生成" />
|
||
</el-form-item>
|
||
<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"
|
||
placeholder="请输入项目编号,不填自动生成"
|
||
:disabled="dialogType === 'edit'"
|
||
/>
|
||
</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" class="project-apply-form__item--full">
|
||
<el-tree-select
|
||
v-model="form.businessDeptId"
|
||
:data="businessDeptTreeOptions"
|
||
:props="deptTreeSelectProps"
|
||
node-key="value"
|
||
placeholder="请选择业务部门"
|
||
check-strictly
|
||
filterable
|
||
clearable
|
||
:render-after-expand="false"
|
||
:disabled="isBasicInfoReadonly"
|
||
@change="handleBusinessDeptChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="承办部门" prop="undertakeDeptId">
|
||
<el-tree-select
|
||
v-model="form.undertakeDeptId"
|
||
:data="deptTreeOptions"
|
||
:props="deptTreeSelectProps"
|
||
node-key="value"
|
||
placeholder="请选择承办部门"
|
||
check-strictly
|
||
clearable
|
||
filterable
|
||
:render-after-expand="false"
|
||
:disabled="isBasicInfoReadonly"
|
||
@change="handleUndertakeDeptChange"
|
||
/>
|
||
</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" class="project-apply-form__item--full">
|
||
<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="支持多选,选择后下方列表展示"
|
||
multiple
|
||
filterable
|
||
clearable
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
: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
|
||
collapse-tags-tooltip
|
||
: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)"
|
||
@blur="formatAmountField('fundLimit')"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="项目应收账款额度(万元)" prop="receivableLimit">
|
||
<el-input
|
||
v-model="form.receivableLimit"
|
||
placeholder="回款期内最大应收账款额度"
|
||
@input="value => handleNumberInput('receivableLimit', value)"
|
||
@blur="formatAmountField('receivableLimit')"
|
||
/>
|
||
</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">
|
||
<template #default="{ row }">
|
||
{{ normalizeReadonlyAmount(row.credit) }}
|
||
</template>
|
||
</el-table-column>
|
||
<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="2"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="盈利模式及利润测算情况说明" prop="profitRemark">
|
||
<el-input
|
||
v-model="form.profitRemark"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="项目风险点" prop="riskPoint">
|
||
<el-input
|
||
v-model="form.riskPoint"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="风险防范措施及应急预案" prop="emergencyPlan">
|
||
<el-input
|
||
v-model="form.emergencyPlan"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="请填写备注信息"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="project-apply-form__material-card">
|
||
<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-alert
|
||
v-if="dialogReadonly && missingRequiredAttachmentTypes.length"
|
||
class="project-apply-form__material-warning"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
:title="`缺少必需材料:${missingRequiredAttachmentTypes.join('、')}`"
|
||
/>
|
||
<el-table
|
||
:data="attachmentRows"
|
||
border
|
||
class="project-apply-form__material-table"
|
||
@selection-change="handleAttachmentSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<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 }">
|
||
<el-link type="primary" @click="previewAttachment(row)">
|
||
{{ attachmentName(row) }}
|
||
</el-link>
|
||
</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" sortable/>
|
||
<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="danger" @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"
|
||
:show-file-list="false"
|
||
button-text="上传附件"
|
||
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过500M"
|
||
@change="handleAttachmentChange"
|
||
/>
|
||
</div>
|
||
|
||
<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" sortable/>
|
||
<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="2"
|
||
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="2"
|
||
placeholder="2000字以内"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</template>
|
||
</el-form>
|
||
|
||
<div
|
||
class="project-apply-dialog__footer"
|
||
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
|
||
>
|
||
<template v-if="isChangeDialog">
|
||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||
<el-button type="primary" plain :loading="submitLoading" @click="saveChangeProject">
|
||
保存
|
||
</el-button>
|
||
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||
<el-button
|
||
v-if="!dialogReadonly"
|
||
type="primary"
|
||
plain
|
||
:loading="submitLoading"
|
||
@click="saveProject"
|
||
>
|
||
保存
|
||
</el-button>
|
||
<el-button
|
||
v-if="!dialogReadonly"
|
||
type="primary"
|
||
:loading="submitLoading"
|
||
@click="submitProject"
|
||
>
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
</component>
|
||
|
||
<el-dialog
|
||
v-model="attachmentDocumentPreviewVisible"
|
||
:title="attachmentPreviewFile.name || '附件预览'"
|
||
append-to-body
|
||
destroy-on-close
|
||
width="90%"
|
||
top="4vh"
|
||
class="project-apply-form__attachment-viewer-dialog"
|
||
>
|
||
<pdf-preview
|
||
v-if="
|
||
attachmentDocumentPreviewVisible &&
|
||
attachmentPreviewFile.url &&
|
||
attachmentPreviewFile.isPdf
|
||
"
|
||
:source="attachmentPreviewFile.url"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
<open-file-viewer
|
||
v-else-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
|
||
:file="attachmentPreviewFile.url"
|
||
:file-name="attachmentPreviewFile.name"
|
||
:mime-type="attachmentPreviewFile.mimeType"
|
||
width="100%"
|
||
height="72vh"
|
||
fit="contain"
|
||
theme="auto"
|
||
locale="zh-CN"
|
||
:toolbar="attachmentViewerToolbar"
|
||
:plugins="attachmentViewerPlugins"
|
||
@unsupported="handleAttachmentPreviewUnsupported"
|
||
@error="handleAttachmentPreviewError"
|
||
/>
|
||
</el-dialog>
|
||
<el-image-viewer
|
||
v-if="attachmentImagePreviewVisible"
|
||
:url-list="attachmentImagePreviewUrls"
|
||
:initial-index="attachmentImagePreviewIndex"
|
||
@close="attachmentImagePreviewVisible = false"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="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 { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||
import { ElImageViewer } from 'element-plus';
|
||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||
import {
|
||
fallbackPlugin,
|
||
imagePlugin,
|
||
officePlugin,
|
||
textPlugin,
|
||
} from '@open-file-viewer/core';
|
||
import '@open-file-viewer/core/style.css';
|
||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||
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: '',
|
||
changeNo: '',
|
||
changeDate: '',
|
||
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 optionalSentinelFields = [
|
||
'projectScale',
|
||
'estimatedProfit',
|
||
'fundDemand',
|
||
'receivableDays',
|
||
'paymentDays',
|
||
'cargoQuantity',
|
||
];
|
||
|
||
const changeTypeOptions = [
|
||
{ label: '项目变更', value: '项目变更' },
|
||
{ label: '项目备案调整', value: '项目备案调整' },
|
||
];
|
||
|
||
const attachmentViewerPlugins = [
|
||
imagePlugin(),
|
||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||
textPlugin(),
|
||
fallbackPlugin(),
|
||
];
|
||
|
||
const normalProjectAttachmentTypeOptions = [
|
||
'利润测算表',
|
||
'单一来源供应商申请表/三方比价表',
|
||
'合同模板',
|
||
'项目操作说明',
|
||
'项目分析报告',
|
||
'项目考察报告',
|
||
].map(item => ({ label: item, value: item }));
|
||
|
||
const majorProjectAttachmentTypeOptions = [
|
||
'会议纪要',
|
||
'评审表决票',
|
||
'利润测算表',
|
||
'单一来源供应商申请表/三方比价表',
|
||
'项目操作说明',
|
||
'项目分析报告',
|
||
'项目考察报告',
|
||
'合同模板',
|
||
'上游尽职调查报告',
|
||
'下游尽职调查报告',
|
||
'业务承诺函',
|
||
].map(item => ({ label: item, value: item }));
|
||
|
||
export default {
|
||
components: {
|
||
ElImageViewer,
|
||
OpenFileViewer,
|
||
PdfPreview,
|
||
},
|
||
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();
|
||
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: [
|
||
{ 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: [
|
||
{ label: '应收账款回款期限', validator: validateDays, trigger: 'blur' },
|
||
],
|
||
paymentDays: [
|
||
{ label: '回款账期', validator: validateDays, trigger: 'blur' },
|
||
],
|
||
projectScale: [{ label: '项目规模', validator: validateAmount, trigger: 'blur' }],
|
||
estimatedProfit: [{ label: '预估利润', validator: validateAmount, trigger: 'blur' }],
|
||
fundDemand: [{ label: '资金需求', validator: validateAmount, trigger: 'blur' }],
|
||
handlerUserId: [
|
||
{
|
||
required: true,
|
||
message: '请选择项目经办人',
|
||
label: '项目经办人',
|
||
nameProp: 'handlerUserName',
|
||
validator: validateUser,
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
principalUserId: [
|
||
{
|
||
required: true,
|
||
message: '请选择项目负责人',
|
||
label: '项目负责人',
|
||
nameProp: 'principalUserName',
|
||
validator: validateUser,
|
||
trigger: 'change',
|
||
},
|
||
],
|
||
changeContent: [
|
||
{ max: 2000, message: '变更内容不能超过2000个字符', trigger: 'blur' },
|
||
],
|
||
changeReason: [
|
||
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
|
||
],
|
||
},
|
||
deptOptions: [],
|
||
businessDeptTreeOptions: [],
|
||
deptTreeOptions: [],
|
||
deptTreeSelectProps: {
|
||
label: 'label',
|
||
value: 'value',
|
||
children: 'children',
|
||
},
|
||
cargoTypeOptions: [],
|
||
selectedCustomerId: [],
|
||
selectedCarrierIds: [],
|
||
customerOptions: [],
|
||
carrierOptions: [],
|
||
customerLoading: false,
|
||
carrierLoading: false,
|
||
customerRows: [],
|
||
carrierRows: [],
|
||
attachmentRows: [],
|
||
selectedAttachmentRows: [],
|
||
attachmentImagePreviewVisible: false,
|
||
attachmentImagePreviewUrls: [],
|
||
attachmentImagePreviewIndex: 0,
|
||
attachmentDocumentPreviewVisible: false,
|
||
attachmentPreviewFile: {},
|
||
attachmentViewerPlugins,
|
||
attachmentViewerToolbar: {
|
||
download: true,
|
||
fullscreen: true,
|
||
print: true,
|
||
rotate: true,
|
||
zoom: true,
|
||
},
|
||
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';
|
||
},
|
||
isProjectFormPage() {
|
||
return this.$route.path === '/business/project-apply/form';
|
||
},
|
||
projectFormContainer() {
|
||
return this.isProjectFormPage ? 'div' : 'el-dialog';
|
||
},
|
||
projectFormContainerProps() {
|
||
if (this.isProjectFormPage) return { class: 'project-apply-page-form' };
|
||
return {
|
||
modelValue: this.projectBox,
|
||
title: this.projectDialogTitle,
|
||
appendToBody: true,
|
||
destroyOnClose: true,
|
||
width: '1440px',
|
||
top: '4vh',
|
||
class: 'project-apply-dialog',
|
||
};
|
||
},
|
||
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;
|
||
},
|
||
missingRequiredAttachmentTypes() {
|
||
const uploadedTypes = new Set(this.attachmentRows.map(item => item.fileType).filter(Boolean));
|
||
return this.projectAttachmentTypeOptions
|
||
.map(item => item.value)
|
||
.filter(fileType => !uploadedTypes.has(fileType));
|
||
},
|
||
},
|
||
created() {
|
||
this.loadDeptOptions();
|
||
this.loadCargoTypeOptions();
|
||
this.loadTransportTypeOptions();
|
||
if (this.isProjectFormPage) {
|
||
this.openProjectFormPage();
|
||
}
|
||
},
|
||
methods: {
|
||
buildTableOption() {
|
||
return {
|
||
...option,
|
||
dialogType: 'dialog',
|
||
addBtn: false,
|
||
viewBtn: false,
|
||
editBtn: false,
|
||
delBtn: false,
|
||
// 操作列收窄:本项目操作按钮较少,320px 过宽,调整为 220px(链接会自动换行兜底)
|
||
menuWidth: 200,
|
||
column: (option.column || []).map(column => ({
|
||
...column,
|
||
display: false,
|
||
align: 'center',
|
||
headerAlign: 'center',
|
||
})),
|
||
};
|
||
},
|
||
hasPermission(code) {
|
||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||
},
|
||
openProjectFormPage() {
|
||
const type = this.$route.query.mode || 'add';
|
||
const id = this.$route.query.id;
|
||
this.dialogType = type;
|
||
this.dialogReadonly = type === 'view';
|
||
this.projectBox = true;
|
||
if (['add', 'majorSupplement'].includes(type)) {
|
||
this.applyProjectDetail({
|
||
...emptyForm(),
|
||
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
|
||
});
|
||
this.fillDefaultUsers();
|
||
return;
|
||
}
|
||
this.api.getDetail(id).then(res => {
|
||
const detail = res.data.data || {};
|
||
const isChange = type === 'change';
|
||
this.applyProjectDetail({
|
||
...detail,
|
||
changeType: isChange ? this.resolveChangeType(detail) : detail.changeType,
|
||
changeNo: isChange ? this.generateChangeNo(detail) : '',
|
||
changeDate: isChange ? this.$dayjs().format('YYYY-MM-DD') : '',
|
||
});
|
||
});
|
||
},
|
||
closeProjectForm() {
|
||
if (this.isProjectFormPage) {
|
||
this.$router.push('/business/project-apply');
|
||
} else {
|
||
this.projectBox = 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] || '未知';
|
||
},
|
||
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 = {}) {
|
||
const query = { mode: type };
|
||
if (['edit', 'view', 'change'].includes(type)) {
|
||
query.id = row.id;
|
||
}
|
||
if (type === 'add') query.name = `新增${this.config.title}`;
|
||
else if (type === 'edit') query.name = `编辑${this.config.title}`;
|
||
else if (type === 'view') query.name = `查看${this.config.title}`;
|
||
else if (type === 'majorSupplement') query.name = '重大项目补录';
|
||
else if (type === 'change') query.name = '项目变更';
|
||
this.$router.push({ path: '/business/project-apply/form', query });
|
||
},
|
||
resetProjectDialog() {
|
||
this.$refs.projectForm?.clearValidate();
|
||
this.form = emptyForm();
|
||
this.customerRows = [];
|
||
this.carrierRows = [];
|
||
this.attachmentRows = [];
|
||
this.selectedAttachmentRows = [];
|
||
this.changeRows = [];
|
||
this.selectedCustomerId = [];
|
||
this.selectedCarrierIds = [];
|
||
},
|
||
applyProjectDetail(row) {
|
||
const displayRow = {
|
||
...row,
|
||
fundLimit: this.formatAmount(row.fundLimit),
|
||
receivableLimit: this.formatAmount(row.receivableLimit),
|
||
projectScale: this.normalizeOptionalSentinel(row.projectScale),
|
||
estimatedProfit: this.normalizeOptionalSentinel(row.estimatedProfit),
|
||
fundDemand: this.normalizeOptionalSentinel(row.fundDemand),
|
||
receivableDays: this.normalizeOptionalSentinel(row.receivableDays),
|
||
paymentDays: this.normalizeOptionalSentinel(row.paymentDays),
|
||
cargoQuantity: this.normalizeOptionalSentinel(row.cargoQuantity),
|
||
};
|
||
const form = {
|
||
...emptyForm(),
|
||
...displayRow,
|
||
handlerUserName: this.resolveUserRealName(displayRow, 'handler'),
|
||
principalUserName: this.resolveUserRealName(displayRow, 'principal'),
|
||
businessDateRange:
|
||
displayRow.businessStartDate && displayRow.businessEndDate
|
||
? [displayRow.businessStartDate, displayRow.businessEndDate]
|
||
: [],
|
||
};
|
||
const situation = this.parseSituation(displayRow.situationRemark);
|
||
const customerRows = this.parseJsonArray(displayRow.customerJson);
|
||
const carrierRows = this.parseJsonArray(displayRow.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(displayRow.attachmentsJson);
|
||
this.selectedAttachmentRows = [];
|
||
this.changeRows = this.buildChangeRows(displayRow);
|
||
this.selectedCustomerId = this.customerRows
|
||
.map(item => this.getCustomerRowId(item))
|
||
.filter(Boolean);
|
||
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?.real_name ||
|
||
this.userInfo?.name ||
|
||
this.userInfo?.userName ||
|
||
this.userInfo?.user_name ||
|
||
this.userInfo?.account ||
|
||
'';
|
||
if (userId && userName) {
|
||
this.form.handlerUserId = userId;
|
||
this.form.handlerUserName = userName;
|
||
this.form.principalUserId = userId;
|
||
this.form.principalUserName = userName;
|
||
}
|
||
},
|
||
resolveUserRealName(row = {}, role) {
|
||
return (
|
||
row[`${role}RealName`] ||
|
||
row[`${role}UserRealName`] ||
|
||
row[`${role}User`]?.realName ||
|
||
row[role]?.realName ||
|
||
row[`${role}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.closeProjectForm();
|
||
this.onLoad(this.page, this.query);
|
||
},
|
||
error => {
|
||
window.console.log(error);
|
||
}
|
||
)
|
||
.finally(() => {
|
||
this.submitLoading = false;
|
||
});
|
||
});
|
||
},
|
||
saveProject() {
|
||
this.submitLoading = true;
|
||
this.api
|
||
.saveDraft(this.normalizeSubmitForm())
|
||
.then(res => {
|
||
if (res.data?.success === false || res.data?.data === false) {
|
||
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
|
||
return;
|
||
}
|
||
this.$message.success('保存成功!');
|
||
this.closeProjectForm();
|
||
this.onLoad(this.page, this.query);
|
||
})
|
||
.finally(() => {
|
||
this.submitLoading = false;
|
||
});
|
||
},
|
||
handleCancelProject() {
|
||
if (this.isProjectFormPage) {
|
||
this.closeProjectForm();
|
||
return;
|
||
}
|
||
if (!['add', 'majorSupplement', 'change'].includes(this.dialogType)) {
|
||
this.closeProjectForm();
|
||
return;
|
||
}
|
||
this.$confirm('确认后直接关闭弹窗,列表无数据变更,是否继续?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}).then(() => {
|
||
this.closeProjectForm();
|
||
});
|
||
},
|
||
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.closeProjectForm();
|
||
this.onLoad(this.page, this.query);
|
||
})
|
||
.finally(() => {
|
||
this.submitLoading = false;
|
||
});
|
||
});
|
||
},
|
||
normalizeSubmitForm(options = {}) {
|
||
const [businessStartDate, businessEndDate] = this.form.businessDateRange || [];
|
||
const submitRow = {
|
||
...this.form,
|
||
fundLimit: this.formatAmount(this.form.fundLimit),
|
||
receivableLimit: this.formatAmount(this.form.receivableLimit),
|
||
...Object.fromEntries(
|
||
optionalSentinelFields.map(key => [key, this.normalizeOptionalSentinel(this.form[key], null)])
|
||
),
|
||
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() {
|
||
this.attachmentRows = this.attachmentRows.map(row => ({
|
||
...row,
|
||
fileType: this.resolveAttachmentFileType(row),
|
||
}));
|
||
},
|
||
normalizeAttachmentTypeText(value) {
|
||
let text = String(value || '').split(/[?#]/)[0];
|
||
try {
|
||
text = decodeURIComponent(text);
|
||
} catch (error) {
|
||
// 文件名可能包含不完整的转义字符,直接使用原始名称继续匹配。
|
||
}
|
||
return text
|
||
.replace(/\.[^.\\/]+$/, '')
|
||
.toLowerCase()
|
||
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
|
||
},
|
||
attachmentTypeKeywords(fileType) {
|
||
if (fileType === '单一来源供应商申请表/三方比价表') {
|
||
return ['单一来源供应商申请表', '三方比价表'];
|
||
}
|
||
return [fileType];
|
||
},
|
||
resolveAttachmentFileType(row = {}) {
|
||
const values = this.projectAttachmentTypeOptions.map(item => item.value);
|
||
if (values.includes(row.fileType)) return row.fileType;
|
||
const fileName = this.normalizeAttachmentTypeText(this.attachmentName(row));
|
||
if (!fileName) return '';
|
||
const option = this.projectAttachmentTypeOptions.find(item =>
|
||
this.attachmentTypeKeywords(item.value).some(keyword =>
|
||
fileName.includes(this.normalizeAttachmentTypeText(keyword))
|
||
)
|
||
);
|
||
return option?.value || '';
|
||
},
|
||
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);
|
||
},
|
||
normalizeReadonlyAmount(value) {
|
||
return this.normalizeOptionalSentinel(value);
|
||
},
|
||
normalizeOptionalSentinel(value, emptyValue = '') {
|
||
return value === null || value === undefined || value === '' || Number(value) === -1
|
||
? emptyValue
|
||
: value;
|
||
},
|
||
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: this.resolveUserRealName(row, 'handler'),
|
||
content: row.changeContent || '',
|
||
reason: row.changeReason || '',
|
||
status: row.approvalStatusName || row.approvalStatus || '',
|
||
action: this.dialogReadonly ? '流程' : '编辑',
|
||
},
|
||
];
|
||
},
|
||
generateChangeNo(detail) {
|
||
const base = detail && detail.projectCode ? detail.projectCode : 'XM';
|
||
return `${base}-BG${this.$dayjs().format('YYYYMMDD')}`;
|
||
},
|
||
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 => {
|
||
const deptTree = res.data.data || [];
|
||
const businessDeptTree = ['add', 'edit', 'majorSupplement'].includes(this.dialogType)
|
||
? this.excludeExternalOrganization(deptTree)
|
||
: deptTree;
|
||
const undertakeDeptTree =
|
||
this.dialogType === 'add' ? this.excludeExternalOrganization(deptTree) : deptTree;
|
||
this.deptOptions = this.flattenDept(businessDeptTree);
|
||
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree);
|
||
this.deptTreeOptions = this.toDeptCascaderOptions(undertakeDeptTree);
|
||
});
|
||
},
|
||
excludeExternalOrganization(list = []) {
|
||
return list.reduce((result, item) => {
|
||
const deptName = item.title || item.deptName || item.name;
|
||
if (String(deptName || '').trim() === '外部组织') return result;
|
||
result.push({
|
||
...item,
|
||
children: item.children?.length
|
||
? this.excludeExternalOrganization(item.children)
|
||
: item.children,
|
||
});
|
||
return result;
|
||
}, []);
|
||
},
|
||
toDeptCascaderOptions(list = []) {
|
||
return list.map(item => ({
|
||
label: item.title || item.deptName || item.name,
|
||
value: item.id,
|
||
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
|
||
}));
|
||
},
|
||
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,
|
||
...(this.dialogType !== 'add' && 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(ids) {
|
||
if (!ids || !ids.length) {
|
||
this.customerRows = [];
|
||
this.form.customerNames = '';
|
||
this.form.customerJson = '';
|
||
return;
|
||
}
|
||
Promise.all(ids.map(id => this.loadCustomerDetailRow(id, 'customer'))).then(rows => {
|
||
this.customerRows = rows;
|
||
this.form.customerNames = rows.map(item => item.customer).join('、');
|
||
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) {
|
||
if (optionalSentinelFields.includes(prop) && Number(value) === -1) {
|
||
this.form[prop] = '';
|
||
return;
|
||
}
|
||
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];
|
||
},
|
||
formatAmount(value) {
|
||
if (value === null || value === undefined || value === '' || Number(value) === -1) return '';
|
||
const amount = Number(value);
|
||
return Number.isFinite(amount) ? amount.toFixed(2) : value;
|
||
},
|
||
formatAmountField(prop) {
|
||
this.form[prop] = this.formatAmount(this.form[prop]);
|
||
},
|
||
handleIntegerInput(prop, value) {
|
||
if (optionalSentinelFields.includes(prop) && Number(value) === -1) {
|
||
this.form[prop] = '';
|
||
return;
|
||
}
|
||
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');
|
||
this.attachmentRows = (list || []).map(item => ({
|
||
...item,
|
||
fileType: this.resolveAttachmentFileType(item),
|
||
description: item.description || '',
|
||
uploadUserName: item.uploadUserName || userName,
|
||
uploadTime: item.uploadTime || uploadTime,
|
||
}));
|
||
this.selectedAttachmentRows = [];
|
||
},
|
||
handleAttachmentSelectionChange(rows) {
|
||
this.selectedAttachmentRows = rows || [];
|
||
},
|
||
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`;
|
||
},
|
||
attachmentUrl(row = {}) {
|
||
return row.url || row.link || row.fileUrl || row.src || row.domain || '';
|
||
},
|
||
attachmentName(row = {}) {
|
||
return row.originalName || row.name || row.fileName || '附件';
|
||
},
|
||
attachmentExtension(row = {}) {
|
||
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||
const index = source.lastIndexOf('.');
|
||
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
|
||
},
|
||
isAttachmentImage(row) {
|
||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(
|
||
this.attachmentExtension(row)
|
||
);
|
||
},
|
||
isAttachmentPdf(row) {
|
||
const mimeType = String(row.mimeType || row.contentType || '').toLowerCase();
|
||
return mimeType.includes('application/pdf') || this.attachmentExtension(row) === 'pdf';
|
||
},
|
||
previewAttachment(row) {
|
||
const url = this.attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空,无法预览');
|
||
return;
|
||
}
|
||
if (this.isAttachmentImage(row)) {
|
||
this.attachmentImagePreviewUrls = this.attachmentRows
|
||
.filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item))
|
||
.map(item => this.attachmentUrl(item));
|
||
this.attachmentImagePreviewIndex = Math.max(
|
||
this.attachmentImagePreviewUrls.indexOf(url),
|
||
0
|
||
);
|
||
this.attachmentImagePreviewVisible = true;
|
||
return;
|
||
}
|
||
const isPdf = this.isAttachmentPdf(row);
|
||
this.attachmentPreviewFile = {
|
||
name: this.attachmentName(row),
|
||
url,
|
||
mimeType: isPdf ? 'application/pdf' : row.mimeType || row.contentType || '',
|
||
isPdf,
|
||
};
|
||
this.attachmentDocumentPreviewVisible = true;
|
||
},
|
||
handleAttachmentPreviewUnsupported() {
|
||
this.$message.warning('当前文件暂不支持在线预览');
|
||
},
|
||
handleAttachmentPreviewError() {
|
||
this.$message.error('附件预览失败');
|
||
},
|
||
downloadAttachment(row) {
|
||
const url = this.attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空');
|
||
return;
|
||
}
|
||
downloadFileByUrl(url, this.attachmentName(row));
|
||
},
|
||
handleBatchDownload() {
|
||
const rows = this.selectedAttachmentRows.length
|
||
? this.selectedAttachmentRows
|
||
: this.attachmentRows;
|
||
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
|
||
if (!downloadableRows.length) {
|
||
this.$message.warning(
|
||
this.selectedAttachmentRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
|
||
);
|
||
return;
|
||
}
|
||
downloadableRows.forEach((row, index) => {
|
||
window.setTimeout(() => this.downloadAttachment(row), index * 300);
|
||
});
|
||
},
|
||
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;
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select),
|
||
:deep(.el-tree-select),
|
||
:deep(.el-date-editor) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
:deep(.el-form-item__label) {
|
||
min-width: 160px;
|
||
white-space: normal;
|
||
line-height: 20px;
|
||
}
|
||
|
||
// 分区标题融入白色卡片顶部(与下方内容块拼成一张完整卡片)
|
||
:deep(.dialog-section-title:not(.project-apply-form__material-title)) {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: #fff;
|
||
border-radius: 6px 6px 0 0;
|
||
padding: 14px 16px 4px;
|
||
margin-bottom: 0;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&__grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
column-gap: 24px;
|
||
row-gap: 0;
|
||
margin-bottom: 12px;
|
||
background: #fff;
|
||
border-radius: 0 0 6px 6px;
|
||
padding: 14px 16px 4px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
|
||
// label 固定宽度:可容纳 6 个汉字,超出自动换行(覆盖全局 min-width:160px 与 auto 行内宽度)
|
||
:deep(.el-form-item__label) {
|
||
width: 108px !important;
|
||
min-width: 0 !important;
|
||
white-space: normal;
|
||
}
|
||
|
||
// 占满整行的表单项(业务部门 / 项目由来说明)
|
||
:deep(.project-apply-form__item--full) {
|
||
grid-column: 1 / -1;
|
||
|
||
.el-form-item__content {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__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: 12px;
|
||
background: #fff;
|
||
border-radius: 0 0 6px 6px;
|
||
padding: 12px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
|
||
:deep(.el-table__header th .cell) {
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
}
|
||
|
||
&__textarea-list {
|
||
margin-bottom: 12px;
|
||
background: #fff;
|
||
border-radius: 0 0 6px 6px;
|
||
padding: 14px 16px 4px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
|
||
: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-bottom: 12px;
|
||
}
|
||
|
||
&__material-card {
|
||
margin-bottom: 12px;
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
padding: 14px 16px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
&__material-table {
|
||
width: 100%;
|
||
margin-bottom: 12px;
|
||
|
||
:deep(.el-table__header th .cell) {
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-color: #eff1f7;
|
||
}
|
||
}
|
||
|
||
&__material-warning {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
&__material-title {
|
||
flex: 1;
|
||
}
|
||
|
||
&__material-warn {
|
||
margin-left: 24px;
|
||
color: #f56c6c;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&__upload {
|
||
margin: 0 0 16px;
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
padding: 12px 0;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
|
||
:deep(.vehicle-attachment-upload) {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
: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;
|
||
}
|
||
}
|
||
|
||
&__change-textarea {
|
||
margin-bottom: 16px;
|
||
background: #fff;
|
||
border-radius: 0 0 6px 6px;
|
||
padding: 14px 16px 4px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
|
||
:deep(.el-form-item__content) {
|
||
width: 100%;
|
||
margin-left: 0 !important;
|
||
}
|
||
}
|
||
}
|
||
|
||
:global(.project-apply-dialog .el-dialog__body) {
|
||
max-height: 76vh;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 16px;
|
||
background: #f5f6fa;
|
||
}
|
||
|
||
.project-apply-page-form {
|
||
min-height: 100%;
|
||
margin-bottom: 60px;
|
||
//padding: 20px 24px 92px;
|
||
//background: #f5f6fa;
|
||
}
|
||
|
||
.project-apply-dialog__footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
|
||
.project-apply-page-form & {
|
||
position: fixed;
|
||
right: 0;
|
||
left: 230px;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
margin: 0;
|
||
padding: 12px 24px;
|
||
border-top: 1px solid #eff1f7;
|
||
background: #fff;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
&--change {
|
||
justify-content: flex-start;
|
||
padding-left: 24px;
|
||
}
|
||
}
|
||
|
||
:global(.avue--collapse .project-apply-page-form .project-apply-dialog__footer) {
|
||
left: 60px;
|
||
}
|
||
|
||
:global(.avue-layout--horizontal .project-apply-page-form .project-apply-dialog__footer) {
|
||
left: 0;
|
||
}
|
||
|
||
.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>
|