项目、合同fix

This commit is contained in:
2026-08-06 17:40:03 +08:00
parent 5e92124710
commit e6d4edf362
15 changed files with 887 additions and 335 deletions

View File

@@ -18,6 +18,7 @@
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@search-icon-change="handleSearchIconChange"
@on-load="onLoad"
>
<template #menu-left>
@@ -113,11 +114,35 @@
<template #contractCategory-label>合同类型</template>
<template v-if="config.enableContractExpiryTags" #search-menu>
<div v-show="searchExpanded" class="business-crud-page__contract-expiry-search">
<span class="business-crud-page__contract-expiry-label">签约类型:</span>
<el-select v-model="contractSignType" clearable placeholder="请选择">
<el-option
v-for="item in contractSignTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="business-crud-page__contract-expiry-tags">
<el-check-tag
v-for="item in contractExpiryTagOptions"
:key="item.value"
:checked="contractExpiryScope === item.value"
@change="handleContractExpiryScopeChange(item.value)"
>
{{ item.label }}{{ contractExpiryStats[item.statKey] || 0 }}
</el-check-tag>
</div>
</div>
</template>
<template #projectName-form>
<el-select
v-model="selectedProjectId"
class="business-crud-page__field"
placeholder="请选择项目名称"
:placeholder="config.projectPlaceholder || '请选择项目名称'"
filterable
clearable
:loading="projectLoading"
@@ -1220,7 +1245,7 @@
<el-cascader
v-model="selectedOrganizationId"
class="business-crud-page__field"
placeholder="请选择所属组织"
:placeholder="config.organizationPlaceholder || '请选择所属组织'"
:options="organizationOptions"
:props="organizationCascaderProps"
clearable
@@ -1401,12 +1426,17 @@
</el-radio-group>
</div>
<el-form
v-if="settlementRuleEnabled"
:model="settlementRuleForm"
label-position="right"
label-width="auto"
class="business-crud-page__module-form business-crud-page__module-form--three"
>
<el-form-item label="账单起始日期" required>
<el-form-item
label="账单起始日期"
required
:error="settlementRuleErrors.billStartDate"
>
<el-date-picker
v-model="settlementRuleForm.billStartDate"
type="date"
@@ -1414,9 +1444,10 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('billStartDate')"
/>
</el-form-item>
<el-form-item label="结算类型" required>
<el-form-item label="结算类型" required :error="settlementRuleErrors.settlementType">
<el-select
v-model="settlementRuleForm.settlementType"
placeholder="请选择结算类型"
@@ -1431,7 +1462,12 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="showSettlementBillCycleType" label="账单周期类型" required>
<el-form-item
v-if="showSettlementBillCycleType"
label="账单周期类型"
required
:error="settlementRuleErrors.billCycleType"
>
<el-select
v-model="settlementRuleForm.billCycleType"
placeholder="请选择账单周期类型"
@@ -1446,11 +1482,17 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日">
<el-form-item
v-if="showSettlementBillCutoffDay"
label="账单截单日"
required
:error="settlementRuleErrors.billCutoffDay"
>
<el-select
v-model="settlementRuleForm.billCutoffDay"
placeholder="请选择账单截单日"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('billCutoffDay')"
>
<el-option
v-for="item in billCutoffDayOptions"
@@ -1460,11 +1502,17 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required>
<el-form-item
v-if="showSettlementCycleDays"
label="周期天数"
required
:error="settlementRuleErrors.cycleDays"
>
<el-select
v-model="settlementRuleForm.cycleDays"
placeholder="请选择周期天数"
:disabled="dialogReadonly"
@change="clearSettlementRuleError('cycleDays')"
>
<el-option
v-for="item in cycleDayOptions"
@@ -2798,7 +2846,7 @@
:title="billingPlanMode === 'edit' ? '编辑计费方案' : '添加计费方案'"
append-to-body
v-model="billingPlanBox"
width="96%"
width="90%"
class="business-crud-page__billing-dialog"
>
<el-form
@@ -2825,6 +2873,7 @@
<el-form-item label="备注">
<el-input
v-model="billingPlanForm.remark"
class="business-crud-page__billing-plan-remark"
maxlength="100"
show-word-limit
:disabled="dialogReadonly"
@@ -3956,6 +4005,7 @@ export default {
cargoImportLoading: false,
billingPlanRows: [],
settlementRuleForm: defaultSettlementRule(),
settlementRuleErrors: {},
reconciliationForm: defaultReconciliation(),
changeRecordRows: [],
billingPlanBox: false,
@@ -4033,9 +4083,23 @@ export default {
total: 0,
},
selectionList: [],
contractExpiryScope: '',
contractSignType: '',
searchExpanded: false,
attachmentUploadMode: false,
contractExpiryStats: {
all: 0,
within30: 0,
within90: 0,
over90: 0,
expired: 0,
},
};
},
created() {
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
}
if (this.config.enableProjectSelect) {
this.loadProjectOptions();
}
@@ -4074,6 +4138,18 @@ export default {
actionButtonLikeSearch() {
return this.config.actionButtonLikeSearch === true;
},
contractSignTypeOptions() {
return this.option.column?.find(column => column.prop === 'signType')?.dicData || [];
},
contractExpiryTagOptions() {
return [
{ label: '全部', value: '', statKey: 'all' },
{ label: '30天内到期', value: 'within30', statKey: 'within30' },
{ label: '90天内到期', value: 'within90', statKey: 'within90' },
{ label: '90天以上', value: 'over90', statKey: 'over90' },
{ label: '已到期', value: 'expired', statKey: 'expired' },
];
},
contractSelectEnabled() {
return this.config.enableContractSelect || this.config.enableTransportPlanForm;
},
@@ -4240,7 +4316,9 @@ export default {
return this.isAdmin;
},
readonlyScope() {
return this.config.enableAllDept && this.allDept === 1;
return (
this.config.enableAllDept && this.config.allDeptReadonly !== false && this.allDept === 1
);
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`) && !this.readonlyScope;
@@ -4299,6 +4377,9 @@ export default {
emitPath: true,
};
},
settlementRuleEnabled() {
return Number(this.settlementRuleForm.autoGenerate) === 1;
},
showSettlementBillCycleType() {
return this.settlementRuleForm.settlementType === '月结';
},
@@ -4535,6 +4616,7 @@ export default {
return;
}
if (action.action === 'edit' || action.action === 'attachmentUpload') {
this.attachmentUploadMode = action.action === 'attachmentUpload';
this.$refs.crud.rowEdit(row, index);
return;
}
@@ -4712,6 +4794,7 @@ export default {
return rule.settlementType === '固定天数周期结算';
},
handleSettlementTypeChange(value) {
this.clearSettlementRuleError('settlementType');
if (value === '月结') {
if (!this.settlementRuleForm.billCycleType) {
this.settlementRuleForm.billCycleType = '固定截单日';
@@ -4729,6 +4812,7 @@ export default {
}
},
handleSettlementBillCycleTypeChange(value) {
this.clearSettlementRuleError('billCycleType');
if (value === '固定截单日') {
this.settlementRuleForm.billCutoffDay = this.settlementRuleForm.billCutoffDay || 25;
return;
@@ -4751,23 +4835,40 @@ export default {
}
return nextRule;
},
clearSettlementRuleError(field) {
if (!this.settlementRuleErrors[field]) return;
this.settlementRuleErrors = {
...this.settlementRuleErrors,
[field]: '',
};
},
validateSettlementRule() {
const rule = this.normalizeSettlementRule(this.settlementRuleForm);
this.settlementRuleErrors = {};
if (this.isBillingFieldEmpty(rule.billStartDate)) {
this.settlementRuleErrors = { billStartDate: '请选择账单起始日期' };
this.$message.warning('请选择账单起始日期');
return false;
}
if (this.isBillingFieldEmpty(rule.settlementType)) {
this.settlementRuleErrors = { settlementType: '请选择结算类型' };
this.$message.warning('请选择结算类型');
return false;
}
if (this.isMonthlySettlement(rule) && this.isBillingFieldEmpty(rule.billCycleType)) {
this.settlementRuleErrors = { billCycleType: '请选择账单周期类型' };
this.$message.warning('请选择账单周期类型');
return false;
}
if (this.isFixedBillCutoffSettlement(rule)) {
if (this.isBillingFieldEmpty(rule.billCutoffDay)) {
this.settlementRuleErrors = { billCutoffDay: '请选择账单截单日' };
this.$message.warning('请选择账单截单日');
return false;
}
const billCutoffDay = Number(rule.billCutoffDay);
if (!Number.isInteger(billCutoffDay) || billCutoffDay < 1 || billCutoffDay > 31) {
this.settlementRuleErrors = { billCutoffDay: '账单截单日必须为1~31的整数' };
this.$message.warning('账单截单日必须为1~31的整数');
return false;
}
@@ -4775,13 +4876,14 @@ export default {
if (this.isFixedCycleSettlement(rule)) {
const cycleDays = Number(rule.cycleDays);
if (!Number.isInteger(cycleDays) || cycleDays < 1 || cycleDays > 365) {
this.settlementRuleErrors = { cycleDays: '周期天数必须为1~365的整数' };
this.$message.warning('周期天数必须为1~365的整数');
return false;
}
}
return true;
},
normalizeRow(row) {
normalizeRow(row, saveAsDraft = false) {
const submitRow = { ...row };
if (this.config.enableTransportPlanForm) {
if (!this.validateTransportPlanForm(submitRow)) return false;
@@ -4801,7 +4903,7 @@ export default {
submitRow.billingPlanJson = JSON.stringify(this.billingPlanRows);
}
if (this.config.enableSettlementRule) {
if (!this.validateSettlementRule()) return false;
if (this.settlementRuleEnabled && !this.validateSettlementRule()) return false;
submitRow.settlementRuleJson = JSON.stringify(this.normalizeSettlementRule());
}
if (this.config.enableReconciliation) {
@@ -4810,7 +4912,7 @@ export default {
if (this.config.enableChangeRecord) {
submitRow.changeRecordJson = JSON.stringify(this.changeRecordRows);
}
if (this.config.saveAsDraft) {
if (this.config.saveAsDraft && saveAsDraft) {
submitRow.approvalStatus = 'draft';
}
delete submitRow.basicInfoTitle;
@@ -4894,22 +4996,25 @@ export default {
this.stopSubmitLoading(loading);
return;
}
this.getSaveRequest()(submitRow).then(
const request =
this.attachmentUploadMode && typeof this.api.updateAttachments === 'function'
? this.api.updateAttachments
: this.getSaveRequest();
request(submitRow).then(
() => {
this.attachmentUploadMode = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
this.attachmentUploadMode = false;
window.console.log(error);
this.stopSubmitLoading(loading);
}
);
},
getSaveRequest() {
if (this.config.saveAsDraft && typeof this.api.saveDraft === 'function') {
return this.api.saveDraft;
}
return this.api.submit;
},
buildContractCreateSubmitRow(action) {
@@ -5031,7 +5136,7 @@ export default {
handleDraftSave() {
const request =
typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
const submitRow = this.normalizeRow(this.form);
const submitRow = this.normalizeRow(this.form, true);
if (!submitRow) return;
this.draftSaveLoading = true;
request(submitRow)
@@ -5094,6 +5199,7 @@ export default {
},
beforeOpen(done, type) {
this.crudDialogType = type;
this.settlementRuleErrors = {};
this.syncCustomCreateDialogButtons(type);
this.dialogReadonly = type === 'view';
this.syncContractNoDisabled(type);
@@ -5175,8 +5281,7 @@ export default {
this.syncCurrentOrganizationOption();
}
if (this.config.enableProjectSelect) {
this.form.usedFundLimit = 0;
this.form.remainingFundLimit = 0;
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
this.syncCurrentProjectOption();
}
if (this.contractSelectEnabled) {
@@ -5223,7 +5328,7 @@ export default {
loadProjectOptions() {
if (!this.config.enableProjectSelect || this.projectLoading) return;
this.projectLoading = true;
getProjectList(1, 9999, {})
getProjectList(1, 9999, this.config.projectQueryParams || {})
.then(res => {
this.projectOptions = extractRecords(res);
this.syncCurrentProjectOption();
@@ -5247,6 +5352,12 @@ export default {
});
}
},
calculateRemainingFundLimit() {
const projectFundLimit = Number(this.form.projectFundLimit);
const usedFundLimit = Number(this.form.usedFundLimit);
if (!Number.isFinite(projectFundLimit) || !Number.isFinite(usedFundLimit)) return '';
return Math.round((projectFundLimit - usedFundLimit + Number.EPSILON) * 100) / 100;
},
handleProjectChange(projectId) {
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
if (!project) {
@@ -5274,7 +5385,7 @@ export default {
this.form.undertakeDeptName = project.undertakeDeptName || '';
this.form.projectFundLimit = project.fundLimit || project.projectFundLimit || 0;
this.form.usedFundLimit = 0;
this.form.remainingFundLimit = 0;
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
if (this.contractSelectEnabled || this.config.enableTransportPlanForm) {
this.form.customerName =
project.customerName ||
@@ -7616,11 +7727,16 @@ export default {
},
searchReset() {
this.query = {};
this.contractExpiryScope = '';
this.contractSignType = '';
this.page.currentPage = 1;
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.query = {
...params,
...(this.contractSignType ? { signType: this.contractSignType } : {}),
};
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
@@ -7643,6 +7759,25 @@ export default {
refreshChange() {
this.onLoad(this.page, this.query);
},
handleSearchIconChange(expanded) {
this.searchExpanded = expanded;
},
handleContractExpiryScopeChange(expireScope) {
this.contractExpiryScope = expireScope;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
},
refreshContractExpiryStats(query) {
if (!this.config.enableContractExpiryTags || typeof this.api.expireStats !== 'function') return;
const statsQuery = { ...query };
delete statsQuery.expireScope;
this.api.expireStats(statsQuery).then(res => {
this.contractExpiryStats = {
...this.contractExpiryStats,
...(res.data.data || {}),
};
});
},
handleScopeChange() {
if (!this.canViewAllDept) {
this.allDept = 0;
@@ -7699,7 +7834,13 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeQueryParams({ ...params, ...this.query, allDept: this.allDept });
const query = this.normalizeQueryParams({
...params,
...this.query,
allDept: this.allDept,
...(this.contractSignType ? { signType: this.contractSignType } : {}),
...(this.contractExpiryScope ? { expireScope: this.contractExpiryScope } : {}),
});
this.api
.getList(page.currentPage, page.pageSize, query)
.then(res => {
@@ -7707,6 +7848,7 @@ export default {
this.page.total = result.total;
this.data = result.records;
this.selectionClear();
this.refreshContractExpiryStats(query);
})
.finally(() => {
this.loading = false;
@@ -8610,6 +8752,38 @@ export default {
width: 100%;
}
&__contract-expiry-search {
display: flex;
order: -1;
align-items: center;
gap: 8px;
margin-right: auto;
min-width: 0;
:deep(.el-select) {
width: 160px;
}
}
&__contract-expiry-label {
flex: 0 0 auto;
min-width: 140px;
color: #606266;
text-align: right;
white-space: nowrap;
}
&__contract-expiry-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
:deep(.el-check-tag) {
margin: 0;
white-space: nowrap;
}
}
.dialog-section-title {
display: flex;
align-items: center;
@@ -9404,6 +9578,10 @@ export default {
margin-bottom: 18px;
}
&__billing-plan-remark {
width: 80%;
}
&__billing-rule-head {
display: flex;
align-items: center;

View File

@@ -174,21 +174,16 @@
</el-select>
</el-form-item>
<el-form-item label="承办部门" prop="undertakeDeptId">
<el-select
<el-cascader
v-model="form.undertakeDeptId"
placeholder="请选择承办部门"
filterable
clearable
filterable
:options="deptTreeOptions"
:props="deptCascaderProps"
:disabled="isBasicInfoReadonly"
@change="handleUndertakeDeptChange"
>
<el-option
v-for="item in deptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
/>
</el-form-item>
<el-form-item label="项目由来" prop="projectSource">
<el-select
@@ -215,9 +210,11 @@
<el-form-item label="客户名称" prop="customerNames">
<el-select
v-model="selectedCustomerId"
placeholder="搜索选择"
placeholder="支持多选,选择后下方列表展示"
multiple
filterable
clearable
collapse-tags
:loading="customerLoading"
@visible-change="visible => visible && loadCustomerOptions('客户')"
@change="handleCustomerChange"
@@ -255,6 +252,7 @@
v-model="form.fundLimit"
placeholder="实际业务回款周期内付款额度"
@input="value => handleNumberInput('fundLimit', value)"
@blur="formatAmountField('fundLimit')"
/>
</el-form-item>
<el-form-item label="项目应收账款额度万元" prop="receivableLimit">
@@ -262,6 +260,7 @@
v-model="form.receivableLimit"
placeholder="回款期内最大应收账款额度"
@input="value => handleNumberInput('receivableLimit', value)"
@blur="formatAmountField('receivableLimit')"
/>
</el-form-item>
<el-form-item label="应收账款回款期限" prop="receivableDays">
@@ -464,65 +463,75 @@
</el-form-item>
</div>
<div class="project-apply-form__material-head">
<div class="dialog-section-title project-apply-form__material-title">项目材料</div>
<el-button type="primary" :disabled="!attachmentRows.length" @click="handleBatchDownload"
>批量下载</el-button
>
</div>
<el-table :data="attachmentRows" border class="project-apply-form__table">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="附件类型" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.fileType" placeholder="请选择" :disabled="dialogReadonly">
<el-option
v-for="item in projectAttachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
<div class="project-apply-form__material-card">
<div class="project-apply-form__material-head">
<div class="dialog-section-title project-apply-form__material-title">项目材料</div>
<el-button
type="primary"
:disabled="!attachmentRows.length"
@click="handleBatchDownload"
>批量下载</el-button
>
</div>
<el-alert
v-if="dialogReadonly && missingRequiredAttachmentTypes.length"
class="project-apply-form__material-warning"
type="warning"
:closable="false"
show-icon
:title="`缺少必需材料:${missingRequiredAttachmentTypes.join('、')}`"
/>
<el-table :data="attachmentRows" border class="project-apply-form__material-table">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="附件类型" min-width="160" align="center">
<template #default="{ row }">
<el-select v-model="row.fileType" placeholder="请选择" :disabled="dialogReadonly">
<el-option
v-for="item in projectAttachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240" align="center">
<template #default="{ row }">
<el-input
v-model="row.description"
placeholder="请输入附件描述"
:disabled="dialogReadonly"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240" align="center">
<template #default="{ row }">
<el-input
v-model="row.description"
placeholder="请输入附件描述"
:disabled="dialogReadonly"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" min-width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" min-width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" min-width="170" align="center" />
<el-table-column label="操作" width="110" align="center">
<template #default="{ row, $index }">
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
<el-link
v-if="!dialogReadonly"
type="primary"
@click="removeAttachment($index)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="attachmentRows"
class="project-apply-form__upload"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
button-text="上传附件"
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件大小限制500M"
@change="handleAttachmentChange"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" min-width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" min-width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" min-width="170" align="center" />
<el-table-column label="操作" width="110" align="center">
<template #default="{ row, $index }">
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
<el-link v-if="!dialogReadonly" type="primary" @click="removeAttachment($index)"
>删除</el-link
>
</template>
</el-table-column>
</el-table>
<vehicle-attachment-upload
v-model="attachmentRows"
class="project-apply-form__upload"
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="50"
button-text="上传附件"
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件大小限制50M"
@change="handleAttachmentChange"
/>
</div>
<div v-if="!isChangeDialog" class="dialog-section-title project-apply-form__table-title">
变更记录
@@ -593,16 +602,24 @@
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
>
<template v-if="isChangeDialog">
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
提交
</el-button>
<el-button type="primary" :loading="submitLoading" @click="saveChangeProject">
保存
</el-button>
<el-button @click="projectBox = false">关闭</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
提交
</el-button>
<el-button @click="handleCancelProject">取消</el-button>
</template>
<template v-else>
<el-button @click="projectBox = false">取消</el-button>
<el-button @click="handleCancelProject">取消</el-button>
<el-button
v-if="!dialogReadonly"
type="primary"
:loading="submitLoading"
@click="saveProject"
>
保存
</el-button>
<el-button
v-if="!dialogReadonly"
type="primary"
@@ -743,6 +760,7 @@ const changeTypeOptions = [
];
const normalProjectAttachmentTypeOptions = [
'项目立项报告',
'利润测算表',
'单一来源供应商申请表/三方比价表',
'合同模板',
@@ -752,6 +770,7 @@ const normalProjectAttachmentTypeOptions = [
].map(item => ({ label: item, value: item }));
const majorProjectAttachmentTypeOptions = [
'项目立项报告',
'会议纪要',
'评审表决票',
'利润测算表',
@@ -873,6 +892,8 @@ export default {
fundDemand: [{ label: '资金需求', validator: validateAmount, trigger: 'blur' }],
handlerUserId: [
{
required: true,
message: '请选择项目经办人',
label: '项目经办人',
nameProp: 'handlerUserName',
validator: validateUser,
@@ -881,6 +902,8 @@ export default {
],
principalUserId: [
{
required: true,
message: '请选择项目负责人',
label: '项目负责人',
nameProp: 'principalUserName',
validator: validateUser,
@@ -897,8 +920,16 @@ export default {
],
},
deptOptions: [],
deptTreeOptions: [],
deptCascaderProps: {
label: 'label',
value: 'value',
children: 'children',
emitPath: false,
checkStrictly: true,
},
cargoTypeOptions: [],
selectedCustomerId: '',
selectedCustomerId: [],
selectedCarrierIds: [],
customerOptions: [],
carrierOptions: [],
@@ -991,6 +1022,12 @@ export default {
? majorProjectAttachmentTypeOptions
: normalProjectAttachmentTypeOptions;
},
missingRequiredAttachmentTypes() {
const uploadedTypes = new Set(this.attachmentRows.map(item => item.fileType).filter(Boolean));
return this.projectAttachmentTypeOptions
.map(item => item.value)
.filter(fileType => !uploadedTypes.has(fileType));
},
},
created() {
this.loadDeptOptions();
@@ -1231,7 +1268,7 @@ export default {
if (type === 'add' || type === 'majorSupplement') {
this.applyProjectDetail({
...emptyForm(),
projectType: type === 'majorSupplement' ? '重大项目' : '',
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
});
this.fillDefaultUsers();
return;
@@ -1251,21 +1288,35 @@ export default {
this.carrierRows = [];
this.attachmentRows = [];
this.changeRows = [];
this.selectedCustomerId = '';
this.selectedCustomerId = [];
this.selectedCarrierIds = [];
},
applyProjectDetail(row) {
const displayRow = {
...row,
fundLimit: this.formatAmount(row.fundLimit),
receivableLimit: this.formatAmount(row.receivableLimit),
...(this.dialogReadonly
? {
projectScale: this.normalizeReadonlyAmount(row.projectScale),
estimatedProfit: this.normalizeReadonlyAmount(row.estimatedProfit),
fundDemand: this.normalizeReadonlyAmount(row.fundDemand),
}
: {}),
};
const form = {
...emptyForm(),
...row,
...displayRow,
handlerUserName: this.resolveUserRealName(displayRow, 'handler'),
principalUserName: this.resolveUserRealName(displayRow, 'principal'),
businessDateRange:
row.businessStartDate && row.businessEndDate
? [row.businessStartDate, row.businessEndDate]
displayRow.businessStartDate && displayRow.businessEndDate
? [displayRow.businessStartDate, displayRow.businessEndDate]
: [],
};
const situation = this.parseSituation(row.situationRemark);
const customerRows = this.parseJsonArray(row.customerJson);
const carrierRows = this.parseJsonArray(row.carrierJson);
const situation = this.parseSituation(displayRow.situationRemark);
const customerRows = this.parseJsonArray(displayRow.customerJson);
const carrierRows = this.parseJsonArray(displayRow.carrierJson);
this.form = {
...form,
...situation,
@@ -1274,9 +1325,11 @@ export default {
};
this.customerRows = customerRows;
this.carrierRows = carrierRows;
this.attachmentRows = this.parseJsonArray(row.attachmentsJson);
this.changeRows = this.buildChangeRows(row);
this.selectedCustomerId = this.getCustomerRowId(this.customerRows[0]);
this.attachmentRows = this.parseJsonArray(displayRow.attachmentsJson);
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);
@@ -1285,7 +1338,13 @@ export default {
fillDefaultUsers() {
const userId = this.userInfo?.userId || this.userInfo?.id || '';
const userName =
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
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;
@@ -1293,6 +1352,16 @@ export default {
this.form.principalUserName = userName;
}
},
resolveUserRealName(row = {}, role) {
return (
row[`${role}RealName`] ||
row[`${role}UserRealName`] ||
row[`${role}User`]?.realName ||
row[role]?.realName ||
row[`${role}UserName`] ||
''
);
},
resolveChangeType(row = {}) {
if (row.changeType) return row.changeType;
const node = row.currentNode || '';
@@ -1328,6 +1397,36 @@ export default {
});
});
},
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.projectBox = false;
this.onLoad(this.page, this.query);
})
.finally(() => {
this.submitLoading = false;
});
},
handleCancelProject() {
if (!['add', 'majorSupplement', 'change'].includes(this.dialogType)) {
this.projectBox = false;
return;
}
this.$confirm('确认后直接关闭弹窗,列表无数据变更,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.projectBox = false;
});
},
saveChangeProject() {
this.submitChangeForm(false);
},
@@ -1364,6 +1463,8 @@ export default {
const [businessStartDate, businessEndDate] = this.form.businessDateRange || [];
const submitRow = {
...this.form,
fundLimit: this.formatAmount(this.form.fundLimit),
receivableLimit: this.formatAmount(this.form.receivableLimit),
businessStartDate,
businessEndDate,
customerJson: JSON.stringify(this.customerRows),
@@ -1477,6 +1578,11 @@ export default {
})
.filter(Boolean);
},
normalizeReadonlyAmount(value) {
return value === null || value === undefined || value === '' || Number(value) === -1
? ''
: value;
},
mergeCustomerOptions(options = [], selectedOptions = []) {
const result = [...selectedOptions];
options.forEach(option => {
@@ -1505,7 +1611,7 @@ export default {
changeType:
row.changeType ||
(String(row.currentNode || '').includes('备案') ? '项目备案调整' : '项目变更'),
handler: row.handlerUserName || '',
handler: this.resolveUserRealName(row, 'handler'),
content: row.changeContent || '',
reason: row.changeReason || '',
status: row.approvalStatusName || row.approvalStatus || '',
@@ -1529,9 +1635,18 @@ export default {
},
loadDeptOptions() {
getDeptTree().then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
const deptTree = res.data.data || [];
this.deptOptions = this.flattenDept(deptTree);
this.deptTreeOptions = this.toDeptCascaderOptions(deptTree);
});
},
toDeptCascaderOptions(list = []) {
return list.map(item => ({
label: item.title || item.deptName || item.name,
value: item.id,
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
}));
},
flattenDept(list = [], level = 0) {
return list.reduce((result, item) => {
const label = item.title || item.deptName || item.name;
@@ -1594,16 +1709,16 @@ export default {
raw: item,
};
},
handleCustomerChange(id) {
if (!id) {
handleCustomerChange(ids) {
if (!ids || !ids.length) {
this.customerRows = [];
this.form.customerNames = '';
this.form.customerJson = '';
return;
}
this.loadCustomerDetailRow(id, 'customer').then(row => {
this.customerRows = [row];
this.form.customerNames = row.customer;
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');
@@ -1656,6 +1771,14 @@ export default {
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) {
this.form[prop] = String(value || '').replace(/[^\d]/g, '');
},
@@ -1836,13 +1959,36 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
&__material-card {
margin-bottom: 16px;
background: #fff;
border-radius: 6px;
padding: 12px 16px;
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) {
background: #f5f7fa;
color: #303133;
font-weight: 600;
}
:deep(.el-table__cell) {
border-color: #eff1f7;
}
}
&__material-warning {
margin-bottom: 12px;
}
&__material-title {
flex: 1;
}