Files
tms-erp-web/src/views/business/project-apply.vue
T

3706 lines
126 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="project-apply-page" :class="{ 'is-public-view': isPublicViewPage }">
<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>
<el-button
type="warning"
plain
v-if="!readonlyScope && hcsImmediateEnabled"
:loading="hcsRepushingId === 'batch'"
@click="handleHcsBatchRepush"
>
<el-icon class="el-icon--left"><Position /></el-icon>批量推送合规
</el-button>
</template>
<template #approvalStatus="{ row }">
{{ displayStatus(row, 'approvalStatus') }}
</template>
<template #hcsSyncStatus="{ row }">
<el-tooltip v-if="row.hcsSyncMsg" placement="top" :content="row.hcsSyncMsg">
<el-tag :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
{{ hcsSyncLabel(row.hcsSyncStatus) }}
</el-tag>
</el-tooltip>
<el-tag
v-else-if="row.hcsSyncStatus"
:type="hcsSyncTagType(row.hcsSyncStatus)"
class="status-text"
>
{{ hcsSyncLabel(row.hcsSyncStatus) }}
</el-tag>
<span v-else>-</span>
</template>
<template #header>
<div class="project-apply-page__risk-search">
<span class="project-apply-page__risk-label">
资金使用风险
<el-tooltip placement="top" effect="dark">
<template #content>
<div>中风险:项目资金额度使用率 ≥ 80%</div>
<div>高风险:项目资金额度使用率 ≥ 90%</div>
</template>
<el-icon class="project-apply-page__risk-help">
<el-icon-question-filled />
</el-icon>
</el-tooltip>
</span>
<el-check-tag
v-for="item in fundRiskTagOptions"
:key="item.value"
:checked="fundRiskScope === item.value"
@change="changeFundRiskScope(item.value)"
>
{{ item.label }}({{ fundRiskStats[item.statKey] || 0 }})
</el-check-tag>
</div>
</template>
<template #fundUseRisk="{ row }">
<span v-if="row.fundUseRisk === 'high'" class="project-apply-page__risk-high">高</span>
<span
v-else-if="row.fundUseRisk === 'medium'"
class="project-apply-page__risk-medium"
>
中
</span>
<span v-else class="project-apply-page__risk-none">-</span>
</template>
<template #fundUseRisk-header>
<span class="project-apply-page__risk-header">
资金使用风险
<el-tooltip placement="top" effect="dark">
<template #content>
<div>中风险:项目资金额度使用率 ≥ 80%</div>
<div>高风险:项目资金额度使用率 ≥ 90%</div>
</template>
<el-icon class="project-apply-page__risk-help">
<el-icon-question-filled />
</el-icon>
</el-tooltip>
</span>
</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
v-if="hcsImmediateEnabled && canHcsRepush(row)"
type="warning"
@click="handleHcsRepush(row)"
>
{{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }}
</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="dialogType === 'add' ? '若不填写,系统自动生成' : '请输入'"
: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">
<el-tooltip
placement="top"
:disabled="!shouldShowBusinessDeptTooltip"
:show-after="200"
>
<template #content>
<div class="project-apply-form__field-tooltip">{{ businessDeptTooltipText }}</div>
</template>
<span class="project-apply-form__field-tooltip-trigger">
<el-cascader
v-model="businessDeptCascaderValue"
:options="businessDeptTreeOptions"
:props="deptCascaderProps"
placeholder="请选择"
filterable
clearable
:disabled="dialogReadonly"
/>
</span>
</el-tooltip>
</el-form-item>
<el-form-item label="平台公司" prop="undertakeDeptId">
<el-tooltip
placement="top"
:disabled="!shouldShowUndertakeDeptTooltip"
:show-after="200"
>
<template #content>
<div class="project-apply-form__field-tooltip">{{ undertakeDeptTooltipText }}</div>
</template>
<span class="project-apply-form__field-tooltip-trigger">
<el-select
v-model="form.undertakeDeptId"
placeholder="请选择"
multiple
collapse-tags
collapse-tags-tooltip
clearable
filterable
:disabled="dialogReadonly"
@change="handleUndertakeDeptChange"
>
<el-option
v-for="item in platformCompanyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</span>
</el-tooltip>
</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"
type="textarea"
rows="2"
placeholder="请输入"
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 prop="fundLimit" class="project-apply-form__tip-label">
<template #label>
<span>项目资金使用额度</span>
<el-tooltip content="实际业务回款周期内付款" placement="top">
<el-icon class="project-apply-form__label-tip"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input
v-model="form.fundLimit"
placeholder="请输入"
@input="value => handleNumberInput('fundLimit', value)"
@blur="formatAmountField('fundLimit')"
>
<template #suffix>万元</template>
</el-input>
</el-form-item>
<el-form-item prop="receivableLimit" class="project-apply-form__tip-label">
<template #label>
<span>项目应收账款额度</span>
<el-tooltip content="回款期内最大应收账款额" placement="top">
<el-icon class="project-apply-form__label-tip"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input
v-model="form.receivableLimit"
placeholder="请输入"
@input="value => handleNumberInput('receivableLimit', value)"
@blur="formatAmountField('receivableLimit')"
>
<template #suffix>万元</template>
</el-input>
</el-form-item>
<el-form-item label="应收账款回款期限" prop="receivableDays">
<el-input
v-model="form.receivableDays"
placeholder="请输入"
@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"
placeholder="请输入"
@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="businessStartDate">
<el-date-picker
v-model="form.businessStartDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="业务周期止" prop="businessEndDate">
<el-date-picker
v-model="form.businessEndDate"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="运输线路" prop="transportRoute">
<el-input v-model="form.transportRoute" placeholder="请输入" 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="businessMode">
<el-select v-model="form.businessMode" placeholder="请选择">
<el-option
v-for="item in businessModeOptions"
: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"
placeholder="请输入"
@input="value => handleNumberInput('projectScale', value)"
>
<template #suffix>万元</template>
</el-input>
</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"
placeholder="请输入"
@input="value => handleNumberInput('estimatedProfit', value)"
>
<template #suffix>万元</template>
</el-input>
</el-form-item>
<el-form-item label="利润率" prop="profitRate">
<el-input
v-model="form.profitRate"
placeholder="请输入"
@input="value => handleNumberInput('profitRate', value)"
>
<template #suffix>%</template>
</el-input>
</el-form-item>
<el-form-item label="履约保证金" prop="fundDemand">
<el-input
v-model="form.fundDemand"
placeholder="请输入"
@input="value => handleNumberInput('fundDemand', value)"
>
<template #suffix>万元</template>
</el-input>
</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">
<template #default="{ row }">
<el-link
type="primary"
:underline="false"
@click="openCustomerArchive(row)"
>
{{ row.customer }}
</el-link>
</template>
</el-table-column>
<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 label="是否广西百强" min-width="140" align="center">
<template #default="{ row }">
{{ formatGuangxiTop100(row.guangxiTop100) }}
</template>
</el-table-column>
<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>
<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">
<template #default="{ row }">
<el-link
type="primary"
:underline="false"
@click="openCustomerArchive(row)"
>
{{ row.carrier }}
</el-link>
</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>
<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="1000"
show-word-limit
/>
</el-form-item>
<el-form-item label="盈利模式及利润测算情况说明" prop="profitRemark">
<el-input
v-model="form.profitRemark"
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="1000"
show-word-limit
/>
</el-form-item>
<el-form-item label="项目风险点" prop="riskPoint">
<el-input
v-model="form.riskPoint"
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="1000"
show-word-limit
/>
</el-form-item>
<el-form-item label="风险防范措施及应急预案" prop="emergencyPlan">
<el-input
v-model="form.emergencyPlan"
type="textarea"
:rows="2"
placeholder="请输入"
maxlength="1000"
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>
<span
v-if="missingRequiredAttachmentTypes.length"
class="project-apply-form__material-warn"
>
未上传:{{ missingRequiredAttachmentTypes.join('、') }}
</span>
<el-button
type="primary"
class="project-apply-form__material-download"
@click="handleBatchDownload"
>批量下载</el-button
>
</div>
<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
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<span
class="attachment-file-name"
role="button"
tabindex="0"
@click="previewAttachment(row)"
@keydown.enter="previewAttachment(row)"
@keydown.space.prevent="previewAttachment(row)"
>{{ attachmentName(row) }}</span>
</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="{ $index }">
<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="50"
:show-file-list="false"
button-text="上传附件"
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
@change="handleAttachmentChange"
/>
</div>
<div
v-if="!isChangeDialog && !isNewProjectDialog"
class="dialog-section-title project-apply-form__table-title"
>
变更记录
</div>
<el-table
v-if="!isChangeDialog && !isNewProjectDialog"
: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="false"
>
<template #default="{ row }">
<el-tooltip placement="top" :show-after="200">
<template #content>
<div class="project-apply-form__change-record-content-tooltip">{{ row.content || '空' }}</div>
</template>
<span class="project-apply-form__change-record-content-cell">{{ row.content || '空' }}</span>
</el-tooltip>
</template>
</el-table-column>
<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>
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
:rows="changeRecordDetailRows"
/>
<template v-if="isChangeDialog">
<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="请输入"
maxlength="2000"
show-word-limit
/>
</el-form-item>
</template>
</el-form>
<div
v-if="!isPublicViewPage"
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
v-if="isProjectChangeModule && !dialogReadonly"
type="primary"
plain
:loading="submitLoading"
@click="saveProjectChangeDraft"
>
保存
</el-button>
<el-button
v-if="!dialogReadonly"
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="searchUsers"
/>
<el-input
v-model="userQuery.phone"
placeholder="手机号"
clearable
@keyup.enter="searchUsers"
/>
<el-button type="primary" @click="searchUsers">查询</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="realName" label="用户姓名" min-width="140" align="center" />
<el-table-column prop="phone" 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>
<el-dialog
v-model="publicCustomerVisible"
title="查看客商档案"
append-to-body
destroy-on-close
width="92%"
top="4vh"
class="project-apply-public-customer-dialog"
>
<customer-archive
v-if="publicCustomerVisible"
:embedded-public-id="publicCustomerId"
/>
</el-dialog>
</basic-container>
</template>
<script>
import { exportBlob } from '@/api/common';
import { defineAsyncComponent } from 'vue';
import * as api from '@/api/business/project-apply';
import {
loadHcsImmediateEnabled,
repush as hcsRepush,
repushBatch as hcsRepushBatch,
} from '@/api/business/hcs';
import { Position } from '@element-plus/icons-vue';
import { getMkPublicDetail } from '@/api/mk-process';
import {
getList as getCustomerArchiveList,
getDetail as getCustomerArchiveDetail,
} from '@/api/vehicle/customer-archive';
import { getBusinessDeptSelect, getDeptTree, getPlatformCompanySelect } 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, isAdministrator } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/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 {
businessModeOptions,
businessTypeOptions,
projectSourceOptions,
projectTypeOptions,
} from '@/option/business/common';
import { config, option } from '@/option/business/project-apply';
import * as projectChangeApi from '@/api/business/project-change';
const emptyForm = () => ({
projectType: '',
projectCode: '',
projectName: '',
projectShortName: '',
businessDeptId: '',
businessDeptName: '',
undertakeDeptId: [],
undertakeDeptName: '',
projectSource: '',
sourceRemark: '',
changeType: '',
changeNo: '',
changeDate: '',
customerNames: '',
carrierNames: '',
fundLimit: '',
receivableLimit: '',
receivableDays: '',
paymentDays: '',
cargoType: '',
cargoQuantity: '',
businessStartDate: '',
businessEndDate: '',
transportRoute: '',
transportType: '',
businessType: '',
businessMode: '国内运输',
projectScale: '',
settlementMode: '',
estimatedProfit: '',
profitRate: '',
fundDemand: '',
handlerUserId: '',
handlerUserName: '',
principalUserId: '',
principalUserName: '',
customerJson: '',
carrierJson: '',
attachmentsJson: '',
situationRemark: '',
projectIntro: '',
profitRemark: '',
riskPoint: '',
emergencyPlan: '',
changeContent: '',
changeReason: '',
});
const optionalSentinelFields = [
'projectScale',
'estimatedProfit',
'profitRate',
'fundDemand',
'receivableDays',
'paymentDays',
'cargoQuantity',
];
const changeTypeOptions = [
{ label: '项目变更', value: '项目变更' },
{ label: '项目备案调整', value: '项目备案调整' },
];
const attachmentViewerPlugins = [
imagePlugin(),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
const projectApplyListRoute = '/business/project-apply';
const projectApplyFormRoute = '/business/project-apply/form';
const projectApplyListRefreshKey = 'project-apply-list-refresh';
const projectChangeListRoute = '/business/project-change';
const projectChangeFormRoute = '/business/project-change/form';
const projectChangeListRefreshKey = 'project-change-list-refresh';
const otherAttachmentType = { label: '其他附件', value: '其他附件' };
const normalProjectAttachmentTypeOptions = [
'利润测算表',
'单一来源供应商申请表/三方比价表',
'合同模板',
'项目操作说明',
'项目分析报告',
'项目考察报告',
].map(item => ({ label: item, value: item })).concat(otherAttachmentType);
const majorProjectAttachmentTypeOptions = [
'会议纪要',
'评审表决票',
'利润测算表',
'单一来源供应商申请表/三方比价表',
'项目操作说明',
'项目分析报告',
'项目考察报告',
'合同模板',
'上游尽职调查报告',
'下游尽职调查报告',
'业务承诺函',
].map(item => ({ label: item, value: item })).concat(otherAttachmentType);
export default {
name: 'ProjectApply',
components: {
CustomerArchive: defineAsyncComponent(() => import('@/views/vehicle/customer-archive.vue')),
ChangeRecordDetailDialog,
ElImageViewer,
OpenFileViewer,
PdfPreview,
Position,
},
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,
businessModeOptions,
settlementModeOptions: [],
changeTypeOptions,
tableForm: {},
tableOption: this.buildTableOption(),
loading: true,
data: [],
query: {},
allDept: 0,
fundRiskScope: '',
fundRiskStats: { high: 0, medium: 0, none: 0 },
fundRiskTagOptions: [
{ label: '高风险', value: 'high', statKey: 'high' },
{ label: '中风险', value: 'medium', statKey: 'medium' },
],
// 合规(HCS)推送状态;受 thirdParty.hcs.push.immediate-enabled 控制显隐
hcsRepushingId: null,
hcsImmediateEnabled: false,
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
// 表单页实例标记:创建时按路由落定,不随 keep-alive 切走后的 $route 变化翻转。
// 否则 isProjectFormPage 变 false 后容器会从 div 切成 el-dialog(append-to-body),
// 盖住后续打开的页面(如客商档案)。
isFormPageInstance: false,
mkEnabled: false,
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: [
{
type: 'array',
required: true,
min: 1,
message: '请选择平台公司',
trigger: 'change',
},
],
businessMode: [{ 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' }],
profitRate: [{ 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',
},
],
changeReason: [
{
validator: (rule, value, callback) => {
if (!this.isChangeDialog) {
callback();
return;
}
if (!value || !String(value).trim()) {
callback(new Error('请输入变更原因'));
return;
}
if (String(value).length > 2000) {
callback(new Error('变更原因不能超过2000个字符'));
return;
}
callback();
},
trigger: 'blur',
},
],
changeType: [
{
validator: (rule, value, callback) => {
if (!this.isChangeDialog) {
callback();
return;
}
if (!value) {
callback(new Error('请选择变更类型'));
return;
}
callback();
},
trigger: 'change',
},
],
projectIntro: [
{ max: 1000, message: '项目简介不能超过1000个字符', trigger: 'blur' },
],
profitRemark: [
{ max: 1000, message: '盈利模式及利润测算情况说明不能超过1000个字符', trigger: 'blur' },
],
riskPoint: [
{ max: 1000, message: '项目风险点不能超过1000个字符', trigger: 'blur' },
],
emergencyPlan: [
{ max: 1000, message: '风险防范措施及应急预案不能超过1000个字符', trigger: 'blur' },
],
},
deptOptions: [],
businessDeptTreeOptions: [],
platformCompanyOptions: [],
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: [],
changeRecordDetailVisible: false,
changeRecordDetail: null,
changeRecordDetailRows: [],
userBox: false,
publicCustomerVisible: false,
publicCustomerId: '',
userPickType: '',
userLoading: false,
userData: [],
userQuery: {
realName: '',
phone: '',
},
userPage: {
currentPage: 1,
pageSize: 10,
total: 0,
},
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
// 部门级联配置:checkStrictly 允许选中任意层级,不必选到末级
deptCascaderProps() {
return {
label: 'label',
value: 'value',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'click',
};
},
// 业务部门:form 存部门 id,级联控件用根到节点的 id 路径,两者在此双向转换
businessDeptCascaderValue: {
get() {
const id = this.form.businessDeptId;
return id ? this.findDeptPath(id, this.businessDeptTreeOptions) || [id] : [];
},
set(path) {
const value = Array.isArray(path)
? path.filter(item => item !== undefined && item !== null && item !== '')
: [];
this.form.businessDeptId = value.length ? value.at(-1) : '';
this.handleBusinessDeptChange(this.form.businessDeptId);
},
},
// 查看态禁用控件无 hover,用完整路径/名称做 tooltip
businessDeptTooltipText() {
const pathIds = this.businessDeptCascaderValue || [];
if (pathIds.length && this.businessDeptTreeOptions?.length) {
const labels = [];
let nodes = this.businessDeptTreeOptions;
for (const id of pathIds) {
const node = (nodes || []).find(item => String(item.value) === String(id));
if (!node) break;
labels.push(node.label);
nodes = node.children;
}
if (labels.length) return labels.join(' / ');
}
return String(this.form.businessDeptName || '').trim();
},
undertakeDeptTooltipText() {
const name = String(this.form.undertakeDeptName || '').trim();
if (name) return name;
return this.resolveUndertakeDeptNames(this.parseUndertakeDeptIds(this.form.undertakeDeptId));
},
shouldShowBusinessDeptTooltip() {
return this.dialogReadonly && !!this.businessDeptTooltipText;
},
shouldShowUndertakeDeptTooltip() {
return this.dialogReadonly && !!this.undertakeDeptTooltipText;
},
permissionList() {
return {
addBtn: this.canCreate,
};
},
isAdmin() {
return isAdministrator(this.userInfo);
},
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';
},
isProjectChangeModule() {
return this.$route.path === projectChangeFormRoute;
},
isChangeDialog() {
return this.dialogType === 'change' || this.isProjectChangeModule;
},
isNewProjectDialog() {
return ['add', 'majorSupplement'].includes(this.dialogType);
},
isProjectFormPage() {
return this.isFormPageInstance;
},
isPublicViewPage() {
return (
this.$route.path === '/business/project-apply/public-view' ||
this.$route.path === '/business/project-change/public-view'
);
},
publicBizType() {
return this.$route.meta?.bizType || 'project-apply';
},
projectFormContainer() {
// 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗
return this.isFormPageInstance ? 'div' : 'el-dialog';
},
projectFormContainerProps() {
if (this.isFormPageInstance) 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 => fileType !== otherAttachmentType.value)
.filter(fileType => !uploadedTypes.has(fileType));
},
},
created() {
if (this.isAdmin) {
this.allDept = 1;
}
if (this.isPublicViewPage) {
this.isFormPageInstance = true;
this.openPublicProjectForm();
return;
}
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
loadHcsImmediateEnabled()
.then(enabled => {
this.hcsImmediateEnabled = !!enabled;
})
.catch(() => {
this.hcsImmediateEnabled = false;
});
this.loadDeptOptions();
this.loadCargoTypeOptions();
this.loadTransportTypeOptions();
this.loadSettlementModeOptions();
if (this.$route.path === projectApplyFormRoute || this.$route.path === projectChangeFormRoute) {
this.isFormPageInstance = true;
this.openProjectFormPage();
}
},
// 标签切走(keep-alive 缓存)或销毁时,收起 append-to-body 的内层弹窗,
// 避免 Teleport 出去的 DOM 继续盖住后续页面。
deactivated() {
this.closeInnerDialogs();
},
activated() {
if (this.isFormPageInstance || this.isPublicViewPage) return;
if (sessionStorage.getItem(projectApplyListRefreshKey)) {
sessionStorage.removeItem(projectApplyListRefreshKey);
this.onLoad(this.page, this.query);
}
},
beforeUnmount() {
this.closeInnerDialogs();
},
watch: {
$route(to, from) {
if (this.isFormPageInstance || this.isPublicViewPage) return;
if (
to.path === projectApplyListRoute &&
from?.path?.includes('/project-apply/form')
) {
sessionStorage.removeItem(projectApplyListRefreshKey);
this.onLoad(this.page, this.query);
}
},
},
methods: {
markProjectApplyListNeedRefresh() {
sessionStorage.setItem(projectApplyListRefreshKey, '1');
},
closeInnerDialogs() {
this.changeRecordDetailVisible = false;
this.attachmentDocumentPreviewVisible = false;
this.attachmentImagePreviewVisible = false;
this.userBox = false;
this.publicCustomerVisible = false;
},
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);
},
openPublicProjectForm() {
const id = this.$route.query.id;
this.dialogType = this.publicBizType === 'project-change' ? 'change' : 'view';
this.dialogReadonly = true;
this.projectBox = true;
if (!id) {
this.$message.error('缺少项目ID');
return;
}
getMkPublicDetail(this.publicBizType, id)
.then(res => {
const detail = res.data?.data || {};
this.applyPublicDictOptions(detail);
const displayDetail = { ...detail };
delete displayDetail.cargoTypeOptions;
delete displayDetail.transportTypeOptions;
delete displayDetail.settlementModeOptions;
this.applyProjectDetail(displayDetail);
})
.catch(() => {
this.$message.error('项目信息加载失败');
});
},
applyPublicDictOptions(detail = {}) {
this.cargoTypeOptions = this.resolvePublicDictOptions(
detail.cargoTypeOptions,
detail.cargoType
);
this.transportTypeOptions = this.resolvePublicDictOptions(
detail.transportTypeOptions,
detail.transportType
);
this.settlementModeOptions = this.resolvePublicDictOptions(
detail.settlementModeOptions,
detail.settlementMode
);
if (detail.businessDeptId) {
const label = detail.businessDeptName || String(detail.businessDeptId);
this.businessDeptTreeOptions = [{ label, value: detail.businessDeptId }];
this.deptOptions = [{ label, rawLabel: label, value: detail.businessDeptId }];
}
const undertakeDeptIds = this.parseUndertakeDeptIds(detail.undertakeDeptId);
const undertakeDeptNames = this.parseUndertakeDeptNames(detail.undertakeDeptName);
if (undertakeDeptIds.length) {
this.platformCompanyOptions = undertakeDeptIds.map((id, index) => ({
label: undertakeDeptNames[index] || String(id),
value: id,
}));
}
},
resolvePublicDictOptions(options, currentValue) {
const list = Array.isArray(options)
? options
.filter(item => item && item.value !== undefined && item.value !== null && item.value !== '')
.map(item => ({
label: item.label || String(item.value),
value: item.value,
}))
: [];
if (
currentValue !== undefined &&
currentValue !== null &&
currentValue !== '' &&
!list.some(item => String(item.value) === String(currentValue))
) {
list.unshift({ label: String(currentValue), value: currentValue });
}
return list;
},
openProjectFormPage() {
const type = this.$route.query.mode || 'add';
const id = this.$route.query.id;
this.dialogType = this.isProjectChangeModule ? (type === 'view' ? 'view' : 'change') : type;
this.dialogReadonly = type === 'view';
this.projectBox = true;
if (this.isProjectChangeModule) {
if (!id) {
this.$message.error('缺少项目变更ID');
return;
}
projectChangeApi.getDetail(id).then(res => {
const detail = res.data.data || {};
this.applyProjectDetail({
...detail,
changeType: detail.changeType || '项目变更',
changeNo: detail.changeNo || '',
changeDate: detail.changeDate || this.$dayjs().format('YYYY-MM-DD'),
});
});
return;
}
if (['add', 'majorSupplement'].includes(type)) {
this.applyProjectDetail({
...emptyForm(),
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
});
this.fillDefaultUsers();
this.fillDefaultBusinessDept();
return;
}
if (!id) 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) {
if (this.isProjectChangeModule) {
sessionStorage.setItem(projectChangeListRefreshKey, '1');
this.$router.$avueRouter?.closeTag?.();
this.$router.push(projectChangeListRoute);
return;
}
this.markProjectApplyListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push(projectApplyListRoute);
} 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 => {
if (operation.mkOnly && !this.mkEnabled) return false;
if (operation.localOnly && this.mkEnabled) return false;
return this.canOperation(operation, row);
});
},
// ==================== 合规(HCS)推送/对账(照“同步金蝶”按钮模式) ====================
hcsSyncLabel(value) {
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
},
hcsSyncTagType(value) {
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
},
canHcsRepush(row) {
return (
this.hcsImmediateEnabled &&
!this.readonlyScope &&
['approved', 'change_approved'].includes(row.approvalStatus) &&
row.hcsSyncStatus !== 'synced'
);
},
handleHcsRepush(row) {
if (this.hcsRepushingId) return;
this.$confirm('确定推送该数据到合同合规系统?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
const loading = this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
this.hcsRepushingId = row.id;
hcsRepush('project', row.id)
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '推送合规失败');
return;
}
this.$message.success(res.data?.data || '推送合规成功');
this.onLoad(this.page, this.query);
})
.catch(() => {
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
})
.finally(() => {
this.hcsRepushingId = null;
loading.close();
});
});
},
handleHcsBatchRepush() {
if (this.hcsRepushingId) return;
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const invalidCount = this.selectionList.filter(item => !this.canHcsRepush(item)).length;
if (invalidCount) {
this.$message.warning(`选中数据中有${invalidCount}条不符合推送条件(仅审批通过且未同步的可推送),请重新选择`);
return;
}
this.$confirm(`确定将选中的${this.selectionList.length}条数据推送到合同合规系统?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
const loading = this.$loading({
lock: true,
text: '正在批量推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
this.hcsRepushingId = 'batch';
hcsRepushBatch('project', this.selectionList.map(item => item.id))
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '批量推送合规失败');
return;
}
this.$message.success(res.data?.data || '批量推送合规完成');
this.onLoad(this.page, this.query);
})
.catch(() => {
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
})
.finally(() => {
this.hcsRepushingId = null;
loading.close();
});
});
},
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,
...(this.fundRiskScope ? { fundUseRisk: this.fundRiskScope } : {}),
})
.then(res => {
const result = res.data.data || {};
this.page.total = result.total || 0;
this.data = result.records || [];
this.selectionClear();
this.refreshFundRiskStats({ ...params, ...this.query, allDept: this.allDept });
})
.finally(() => {
this.loading = false;
});
},
searchReset() {
this.query = {};
this.fundRiskScope = '';
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
// 切换「资金使用风险」快速筛选:再次点击已选中项则取消筛选
changeFundRiskScope(value) {
this.fundRiskScope = this.fundRiskScope === value ? '' : value;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
},
refreshFundRiskStats(params) {
this.api.getFundRiskStats(params).then(res => {
const stats = res.data?.data || {};
this.fundRiskStats = { ...this.fundRiskStats, ...stats };
});
},
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.startIndependentProjectChange(row);
return;
}
if (operation.action === 'mkAudit') {
this.handleMkAudit(row);
return;
}
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
const request = () => this.api[operation.action](...args);
// 先本地提交再调 MK:MK 成功后会把状态同步成 reviewing,
// 若先 MK 再本地,已撤回/已驳回单据会被「当前状态不允许编辑」拦住。
const afterMk =
operation.action === 'submitApproval'
? request().then(() =>
submitMkApprovalFlow({
bizType: 'project-apply',
bizId: row.id,
subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
tempVarData: { projectType: row.projectType || '' },
})
)
: request();
// 项目通过会同步等待合规推送,加遮罩防重复点击
const loading =
operation.action === 'approve'
? this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
})
: null;
afterMk
.then(res => {
// data=false 表示审批已通过但合规推送失败,msg 带具体原因(照正式结算单审批)
if (operation.action === 'approve' && res.data?.data === false) {
this.$message.warning(res.data?.msg);
this.onLoad(this.page, this.query);
return;
}
if (res.data?.success === false) {
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
return;
}
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
})
.finally(() => loading && loading.close());
};
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());
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('项目ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
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: projectApplyFormRoute, 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),
profitRate: this.normalizeOptionalSentinel(row.profitRate),
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'),
};
const situation = this.parseSituation(displayRow.situationRemark);
const customerRows = this.parseJsonArray(displayRow.customerJson);
const carrierRows = this.parseJsonArray(displayRow.carrierJson);
this.form = {
...form,
...situation,
undertakeDeptId: this.parseUndertakeDeptIds(form.undertakeDeptId),
undertakeDeptName: form.undertakeDeptName || '',
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.normalizeAttachmentFileTypes();
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();
this.ensureCurrentPlatformOption();
this.ensureCurrentBusinessDeptOption();
},
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;
}
},
/**
* 新增/重大补录:业务部门默认取当前用户所属部门中第一个「是否业务部门=是」的组织。
*/
fillDefaultBusinessDept() {
if (!['add', 'majorSupplement'].includes(this.dialogType)) return;
if (this.form.businessDeptId) return;
const raw = this.userInfo?.deptId || this.userInfo?.dept_id || '';
const userDeptIds = String(raw)
.split(/[,,]/)
.map(item => String(item || '').trim())
.filter(item => item && item !== '-1');
if (!userDeptIds.length || !this.deptOptions.length) return;
const matched = userDeptIds.find(id =>
this.deptOptions.some(item => String(item.value) === String(id))
);
if (!matched) return;
this.form.businessDeptId = matched;
this.handleBusinessDeptChange(matched);
},
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;
}
const data = res.data?.data || {};
const id = data.id || this.form.id;
const name = data.projectName || this.form.projectName || '';
// 必须用提交前状态:业务 submit 后可能已变成 reviewing,导致无法识别「通过后再提交」
const status = this.form.approvalStatus || data.approvalStatus || '';
if (!id) {
this.$message.success('操作成功!');
this.closeProjectForm();
this.onLoad(this.page, this.query);
return;
}
return submitMkApprovalFlow({
bizType: 'project-apply',
bizId: id,
subjectName: name,
approvalStatus: status,
mkEnabled: this.mkEnabled,
tempVarData: {
projectType: this.form.projectType || data.projectType || '',
},
}).then(() => {
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();
});
},
startIndependentProjectChange(row) {
if (!row?.id) {
this.$message.warning('项目ID为空,无法发起变更');
return;
}
const loading = this.$loading({
lock: true,
text: '正在创建项目变更单…',
background: 'rgba(255, 255, 255, 0.6)',
});
projectChangeApi
.createFromProject(row.id)
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '创建项目变更失败');
return;
}
const detail = res.data?.data || {};
if (!detail.id) {
this.$message.error('创建项目变更失败:未返回变更单ID');
return;
}
this.$router.push({
path: projectChangeFormRoute,
query: { mode: 'edit', id: detail.id, name: '项目变更' },
});
})
.finally(() => loading.close());
},
saveProjectChangeDraft() {
if (!this.isProjectChangeModule) return;
this.submitLoading = true;
projectChangeApi
.saveDraft(this.normalizeSubmitForm({ includeChangeType: true }))
.then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
return;
}
this.$message.success('保存成功!');
this.closeProjectForm();
})
.finally(() => {
this.submitLoading = false;
});
},
submitChangeProject() {
this.$refs.projectForm.validate(valid => {
if (!valid) {
this.handleValidateFail();
return;
}
if (!this.validateAttachmentFileTypes()) return;
this.submitLoading = true;
if (this.isProjectChangeModule) {
const payload = this.normalizeSubmitForm({ includeChangeType: true });
const approvalStatus = this.form.approvalStatus || '';
projectChangeApi
.submit(payload)
.then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || '提交失败,请联系管理员');
return;
}
const id = this.form.id;
return submitMkApprovalFlow({
bizType: 'project-change',
bizId: id,
subjectName: this.form.projectName || '',
approvalStatus,
mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' },
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
});
})
.finally(() => {
this.submitLoading = false;
});
return;
}
// 兼容旧入口:变更记录仅在提交时生成,保存草稿不落记录
this.api
.submitChange(this.normalizeSubmitForm({ includeChangeType: true }))
.then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || '提交失败,请联系管理员');
return;
}
const id = this.form.id;
return submitMkApprovalFlow({
bizType: 'project-apply',
bizId: id,
subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
mkEnabled: this.mkEnabled,
tempVarData: { projectType: this.form.projectType || '' },
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
this.onLoad(this.page, this.query);
});
})
.finally(() => {
this.submitLoading = false;
});
});
},
normalizeSubmitForm(options = {}) {
const undertakeDeptIds = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
const undertakeDeptName =
this.resolveUndertakeDeptNames(undertakeDeptIds) || this.form.undertakeDeptName || '';
const submitRow = {
...this.form,
undertakeDeptId: undertakeDeptIds.join(','),
undertakeDeptName,
fundLimit: this.formatAmount(this.form.fundLimit),
receivableLimit: this.formatAmount(this.form.receivableLimit),
...Object.fromEntries(
optionalSentinelFields.map(key => [key, this.normalizeOptionalSentinel(this.form[key], null)])
),
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 || '',
}),
};
if (!options.includeChangeType) {
delete submitRow.changeType;
}
// 变更内容字段已下线,提交时始终清空,避免沿用历史值
submitRow.changeContent = '';
if (this.isNewProjectDialog && !options.includeChangeType) {
delete submitRow.changeReason;
}
['projectIntro', 'profitRemark', 'riskPoint', 'emergencyPlan'].forEach(
key => delete submitRow[key]
);
delete submitRow.changeRecordJson;
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 otherAttachmentType.value;
const option = this.projectAttachmentTypeOptions.find(item =>
this.attachmentTypeKeywords(item.value).some(keyword =>
fileName.includes(this.normalizeAttachmentTypeText(keyword))
)
);
return option?.value || otherAttachmentType.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);
if (Array.isArray(list)) return list;
return list && typeof list === 'object' ? [list] : [];
} catch (error) {
return [];
}
},
getCustomerRowId(row = {}) {
return row.id || row.customerId || row.carrierId || '';
},
openCustomerArchive(row = {}) {
const id = this.getCustomerRowId(row);
if (!id) {
this.$message.warning('客商档案 ID 为空,无法打开');
return;
}
if (this.isPublicViewPage) {
this.publicCustomerId = String(id);
this.publicCustomerVisible = true;
return;
}
this.$router.push({
path: '/vehicle/customer-archive/form',
query: { id: String(id), name: '查看客商档案', view: '1' },
});
},
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);
},
formatGuangxiTop100(value) {
if (value === 1 || value === '1') return '是';
if (value === 0 || value === '0') return '否';
return 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) {
const records = this.parseJsonArray(row?.changeRecordJson);
return records.map((record, recordIndex) => {
const changeRow = {
...record,
projectId: row?.id,
recordIndex,
changeDate: record.changeDate || record.changeTime || record.createTime || '',
changeType: record.changeType || '项目变更',
handler: record.handlerUserName || record.changeUserName || record.handler || '',
reason: record.changeReason || '',
status: record.statusName || record.status || '',
action: '查看',
};
return { ...changeRow, content: this.formatChangeContent(changeRow) };
});
},
parseChangeData(value) {
if (!value || value === '-1') return {};
if (typeof value === 'object') return value;
const text = String(value).trim();
if (!text || text === '-1') return {};
try {
const parsed = JSON.parse(text);
return parsed && typeof parsed === 'object' ? parsed : { 变更内容: parsed };
} catch (error) {
return { 变更内容: text };
}
},
getProjectChangeFieldLabel(field) {
const labels = {
projectType: '项目类型',
projectName: '项目名称',
projectShortName: '项目简称',
businessDeptName: '业务部门',
undertakeDeptName: '平台公司',
projectSource: '项目由来',
sourceRemark: '项目由来说明',
fundLimit: '项目资金使用额度',
receivableLimit: '项目应收账款额度',
receivableDays: '应收账款回款期限',
paymentDays: '回款账期',
cargoType: '货物类型',
cargoQuantity: '预估货物数量',
businessStartDate: '业务周期起',
businessEndDate: '业务周期止',
transportRoute: '运输线路',
transportType: '运输类型',
businessType: '业务类型',
businessMode: '业务模式',
projectScale: '项目规模',
settlementMode: '结算方式',
estimatedProfit: '预计利润',
profitRate: '利润率',
fundDemand: '履约保证金',
handlerUserName: '项目经办人',
principalUserName: '项目负责人',
customerNames: '客户名称',
carrierNames: '下游承运商',
situationRemark: '项目情况说明',
attachmentsJson: '项目材料',
};
return labels[field] || field;
},
normalizeProjectChangeValue(value) {
if (typeof value !== 'string') return value;
const text = value.trim();
if (!text || (!text.startsWith('[') && !text.startsWith('{') && text !== 'null')) {
return value;
}
try {
return JSON.parse(text);
} catch (error) {
return value;
}
},
isEmptyProjectChangeValue(value) {
const normalized = this.normalizeProjectChangeValue(value);
return (
normalized === undefined ||
normalized === null ||
normalized === '' ||
normalized === '-1' ||
(Array.isArray(normalized) && normalized.length === 0) ||
(typeof normalized === 'object' && !Array.isArray(normalized) && Object.keys(normalized).length === 0)
);
},
isSameProjectChangeValue(before, after) {
if (this.isEmptyProjectChangeValue(before) && this.isEmptyProjectChangeValue(after)) {
return true;
}
return JSON.stringify(this.normalizeProjectChangeValue(before)) === JSON.stringify(this.normalizeProjectChangeValue(after));
},
formatProjectChangeValue(field, value) {
const normalized = this.normalizeProjectChangeValue(value);
if (this.isEmptyProjectChangeValue(normalized)) return '空';
if (field === 'transportType') {
const option = (this.transportTypeOptions || []).find(
item => String(item.value) === String(normalized)
);
return option?.label || String(normalized);
}
if (field === 'settlementMode') {
const option = (this.settlementModeOptions || []).find(
item => String(item.value) === String(normalized)
);
return option?.label || String(normalized);
}
if (field === 'attachmentsJson') {
const attachments = Array.isArray(normalized)
? normalized
: normalized && typeof normalized === 'object'
? [normalized]
: [];
const names = attachments
.map(item => (typeof item === 'string' ? item : item?.originalName || item?.name || item?.fileName || item?.url || ''))
.map(item => String(item).split('?')[0].split('/').pop())
.filter(Boolean);
if (names.length) return names.join('、');
}
if (Array.isArray(normalized)) {
return normalized.map(item => this.formatProjectChangeValue(field, item)).join('、');
}
if (typeof normalized === 'object') return JSON.stringify(normalized);
return String(normalized);
},
buildSituationChangeRows(beforeValue, afterValue) {
const before = this.parseSituation(beforeValue);
const after = this.parseSituation(afterValue);
const labels = {
projectIntro: '项目简介',
profitRemark: '盈利模式及利润测算情况说明',
riskPoint: '项目风险点',
emergencyPlan: '风险防范措施及应急预案',
};
return Object.keys(labels)
.filter(key => !this.isSameProjectChangeValue(before[key], after[key]))
.map(key => ({
field: labels[key],
before: this.formatProjectChangeValue(key, before[key]),
after: this.formatProjectChangeValue(key, after[key]),
}));
},
buildChangeRecordDetailRows(row = {}) {
const beforeData = this.parseChangeData(row.beforeData);
const afterData = this.parseChangeData(row.afterData);
const fields = [
...new Set([
...(Array.isArray(row.changedFields) ? row.changedFields : []),
...Object.keys(beforeData),
...Object.keys(afterData),
]),
];
const rows = [];
fields
.filter(field => field !== '变更内容' && !this.isSameProjectChangeValue(beforeData[field], afterData[field]))
.forEach(field => {
if (field === 'situationRemark') {
rows.push(...this.buildSituationChangeRows(beforeData[field], afterData[field]));
return;
}
rows.push({
field: this.getProjectChangeFieldLabel(field),
before: this.formatProjectChangeValue(field, beforeData[field]),
after: this.formatProjectChangeValue(field, afterData[field]),
});
});
if (!rows.length && String(row.changeContent || '').trim()) {
rows.push({ field: '变更内容', before: '空', after: String(row.changeContent).trim() });
}
return rows;
},
formatChangeContent(row = {}) {
const rows = this.buildChangeRecordDetailRows(row);
return rows
.filter(item => item.field !== '变更内容')
.map(item => `${item.field}:变更前:${item.before} → 变更后:${item.after}`)
.join(';') || String(row.changeContent || '').trim();
},
generateChangeNo(detail) {
const base = detail && detail.projectCode ? detail.projectCode : 'XM';
return `${base}-BG${this.$dayjs().format('YYYYMMDD')}`;
},
handleBusinessDeptChange(value) {
// 兼容级联路径数组(取末级)与单个部门 id
const id = Array.isArray(value) ? value.at(-1) : value;
const dept = this.deptOptions.find(item => String(item.value) === String(id));
this.form.businessDeptName = dept ? dept.rawLabel : '';
this.$refs.projectForm?.validateField('businessDeptId');
},
handleUndertakeDeptChange(value) {
const ids = this.parseUndertakeDeptIds(value);
this.form.undertakeDeptId = ids;
this.form.undertakeDeptName = this.resolveUndertakeDeptNames(ids);
this.$refs.projectForm?.validateField('undertakeDeptId');
},
parseUndertakeDeptIds(value) {
if (Array.isArray(value)) {
return value
.map(item => String(item ?? '').trim())
.filter(item => item && item !== '-1');
}
return String(value ?? '')
.split(/[,,]/)
.map(item => item.trim())
.filter(item => item && item !== '-1');
},
parseUndertakeDeptNames(value) {
return String(value ?? '')
.split(/[,,、]/)
.map(item => item.trim())
.filter(Boolean);
},
resolveUndertakeDeptNames(ids = []) {
return ids
.map(id => {
const dept = this.platformCompanyOptions.find(item => String(item.value) === String(id));
return dept?.label || '';
})
.filter(Boolean)
.join('、');
},
firstUndertakeDeptId(value) {
return this.parseUndertakeDeptIds(value)[0] || '';
},
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
findDeptPath(id, tree = [], parents = []) {
const target = Array.isArray(id) ? id.at(-1) : id;
if (target === undefined || target === null || target === '') return null;
for (const node of tree || []) {
const path = [...parents, node.value];
if (String(node.value) === String(target)) return path;
const matched = this.findDeptPath(target, node.children, path);
if (matched) return matched;
}
return null;
},
handleProjectTypeChange() {
this.normalizeAttachmentFileTypes();
this.$refs.projectForm?.validateField('projectType');
},
loadDeptOptions() {
Promise.all([getDeptTree(), getBusinessDeptSelect()])
.then(([treeRes, bizRes]) => {
const deptTree = this.excludeExternalOrganization(treeRes.data.data || []);
const bizList = bizRes.data.data || [];
const bizIds = new Set(
bizList
.map(item => (item.id == null || item.id === '' ? '' : String(item.id)))
.filter(Boolean)
);
// 仅保留「是否业务部门=是」的节点及其祖先,祖先不可选
const businessDeptTree = this.filterBusinessDeptTree(deptTree, bizIds);
this.deptOptions = this.flattenDept(businessDeptTree).filter(item =>
bizIds.has(String(item.value))
);
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree, bizIds);
this.fillDefaultBusinessDept();
this.ensureCurrentBusinessDeptOption();
})
.catch(() => {
this.deptOptions = [];
this.businessDeptTreeOptions = [];
});
this.loadPlatformCompanyOptions();
},
/**
* 剪枝组织树:只保留业务部门节点及其祖先路径。
*/
filterBusinessDeptTree(list = [], bizIds = new Set()) {
return (list || []).reduce((result, item) => {
const children = item.children?.length
? this.filterBusinessDeptTree(item.children, bizIds)
: [];
const isBiz = bizIds.has(String(item.id));
if (!isBiz && !children.length) {
return result;
}
result.push({
...item,
children: children.length ? children : undefined,
});
return result;
}, []);
},
ensureCurrentBusinessDeptOption() {
const id = this.form.businessDeptId;
if (!id) return;
const exists = this.deptOptions.some(item => String(item.value) === String(id));
if (exists) return;
const label = this.form.businessDeptName || String(id);
this.deptOptions.unshift({ label, rawLabel: label, value: id });
const pathExists = this.findDeptPath(id, this.businessDeptTreeOptions);
if (!pathExists) {
this.businessDeptTreeOptions.unshift({ label, value: id });
}
},
loadPlatformCompanyOptions() {
getPlatformCompanySelect().then(res => {
const list = res.data.data || [];
this.platformCompanyOptions = list.map(item => ({
label: item.deptName || item.fullName || item.title || item.name,
// 雪花 ID 必须以字符串绑定,避免 JS Number 精度丢失导致多选提交异常
value: item.id == null || item.id === '' ? '' : String(item.id),
})).filter(item => item.value);
this.ensureCurrentPlatformOption();
});
},
ensureCurrentPlatformOption() {
const ids = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
if (!ids.length) return;
const names = this.parseUndertakeDeptNames(this.form.undertakeDeptName);
ids.forEach((id, index) => {
const exists = this.platformCompanyOptions.some(item => String(item.value) === String(id));
if (!exists) {
this.platformCompanyOptions.unshift({
label: names[index] || String(id),
value: id,
});
}
});
// 选项 value 与已选 ID 类型对齐,避免回显失败
this.form.undertakeDeptId = ids.map(id => {
const matched = this.platformCompanyOptions.find(item => String(item.value) === String(id));
return matched ? matched.value : id;
});
},
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 = [], bizIds = null) {
return list.map(item => {
const value = item.id == null || item.id === '' ? item.value : item.id;
const option = {
label: item.title || item.deptName || item.name || item.label,
value: value == null || value === '' ? '' : String(value),
children: item.children?.length
? this.toDeptCascaderOptions(item.children, bizIds)
: undefined,
};
// 仅业务部门可选;祖先节点保留路径但禁用
if (bizIds && !bizIds.has(String(option.value))) {
option.disabled = true;
}
return option;
});
},
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() {
return getBizDictionary({ code: 'transport_type' }).then(res => {
this.transportTypeOptions = (res.data.data || []).map(item => ({
label: item.dictValue,
value: item.dictKey,
}));
});
},
loadSettlementModeOptions() {
return getBizDictionary({ code: 'settle_method' }).then(res => {
const data = res.data?.data || [];
this.settlementModeOptions = data.map(item => ({
label: item.dictValue,
value: item.dictKey,
}));
const column = this.findColumn(this.tableOption.column, 'settlementMode');
if (column) column.dicData = this.settlementModeOptions;
});
},
loadCustomerOptions(type) {
const loadingKey = type === '客户' ? 'customerLoading' : 'carrierLoading';
const optionsKey = type === '客户' ? 'customerOptions' : 'carrierOptions';
this[loadingKey] = true;
getCustomerArchiveList(1, 100, {
customerType: type,
status: 1,
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 || '',
guangxiTop100:
item.guangxiTop100 === 0 || item.guangxiTop100 === 1 ? item.guangxiTop100 : null,
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);
});
},
async handleChangeRecordAction(row) {
if (!row?.projectId && !row?.id) {
this.$message.warning('项目ID为空,无法查看变更详情');
return;
}
if (this.isPublicViewPage) {
this.changeRecordDetail = { ...row };
this.changeRecordDetailRows = this.buildChangeRecordDetailRows(this.changeRecordDetail);
this.changeRecordDetailVisible = true;
return;
}
try {
if (!this.transportTypeOptions.length) {
await this.loadTransportTypeOptions();
}
if (!this.settlementModeOptions.length) {
await this.loadSettlementModeOptions();
}
const response = await api.getChangeRecordDetail(row.projectId || row.id, row.recordIndex);
const detail = response?.data?.data || {};
this.changeRecordDetail = { ...row, ...detail };
this.changeRecordDetailRows = this.buildChangeRecordDetailRows(this.changeRecordDetail);
this.changeRecordDetailVisible = true;
} catch (error) {
this.$message.error('变更记录详情加载失败');
}
},
openUserPicker(type) {
if (this.dialogReadonly) return;
this.userPickType = type;
this.userBox = true;
this.userPage.currentPage = 1;
this.loadUsers();
},
searchUsers() {
this.userPage.currentPage = 1;
this.loadUsers();
},
loadUsers() {
this.userLoading = true;
const params = {
realName: String(this.userQuery.realName || '').trim(),
phone: String(this.userQuery.phone || '').trim(),
};
getUserList(this.userPage.currentPage, this.userPage.pageSize, params)
.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: '', phone: '' };
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-page.is-public-view {
:deep(.el-link) {
pointer-events: auto;
}
}
.project-apply-form {
padding: 0;
:deep(.el-input),
:deep(.el-select),
:deep(.el-tree-select),
:deep(.el-cascader),
:deep(.el-date-editor) {
width: 100%;
}
&__field-tooltip-trigger {
display: block;
width: 100%;
}
&__field-tooltip {
max-width: 480px;
max-height: 240px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
:deep(.el-form-item) {
//margin-bottom: 24px;
}
: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;
}
&__tip-label {
// 保持 .el-form-item__label 默认 inline 布局,让文字按全局 white-space: normal 换行显示
:deep(.el-form-item__label) {
vertical-align: top;
}
}
&__label-tip {
margin-left: 4px;
color: #a8abb2;
cursor: help;
font-size: 14px;
vertical-align: -0.125em;
}
&__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;
}
}
&__change-record-content-cell {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__change-record-content-tooltip {
max-width: 900px;
max-height: 320px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
&__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);
// 与项目基本信息网格使用相同的 6 字符 label 宽度,长文本自动换行
:deep(.el-form-item__label) {
width: 108px !important;
min-width: 0 !important;
white-space: normal;
}
:deep(.el-form-item) {
align-items: flex-start;
}
:deep(.el-form-item__content) {
width: calc(100% - 108px);
}
}
&__material-head {
display: flex;
align-items: center;
justify-content: flex-start;
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-title {
flex: 0 0 auto;
}
&__material-warn {
margin-left: 8px;
color: #f56c6c;
font-size: 14px;
font-weight: 500;
}
&__material-download {
margin-left: auto;
}
&__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;
}
.project-apply-page {
&__risk-search {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
margin-bottom: 16px;
:deep(.el-check-tag) {
font-weight: 500;
}
}
&__risk-label {
display: inline-flex;
align-items: center;
color: #606266;
font-weight: 600;
margin-right: 4px;
}
&__risk-header {
display: inline-flex;
align-items: center;
gap: 4px;
}
&__risk-help {
margin-left: 4px;
color: #a8abb2;
cursor: help;
font-size: 14px;
}
&__risk-none {
color: #c0c4cc;
}
&__risk-high {
color: #f56c6c;
}
&__risk-medium {
color: #e6a23c;
}
}
.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-end;
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>
<style lang="scss">
.project-apply-public-customer-dialog .el-dialog__body {
max-height: 78vh;
overflow: auto;
padding-top: 8px;
}
</style>