|
|
|
|
@ -1,21 +1,45 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div>
|
|
|
|
|
<Modal width="720" v-model="isShow" :title="modalTitle" @on-ok="submit">
|
|
|
|
|
<div style="margin-bottom: 10px;display: flex;justify-content: space-between;align-items: center;">
|
|
|
|
|
<Modal width="720" v-model="isShow" :title="modalTitle" :footer-hide="true">
|
|
|
|
|
<div class="modal-toolbar">
|
|
|
|
|
<span>源项目:{{ row.name }}</span>
|
|
|
|
|
<el-button type="primary" icon="el-icon-plus" size="small" @click="addSection">新增标段</el-button>
|
|
|
|
|
<div class="modal-toolbar-actions">
|
|
|
|
|
<el-button
|
|
|
|
|
v-if="isContinueMode && existingSections.length"
|
|
|
|
|
size="small"
|
|
|
|
|
@click="toggleEditSectionNo"
|
|
|
|
|
>{{ isEditingSectionNo ? '取消编辑' : '编辑标段' }}</el-button>
|
|
|
|
|
<el-button type="primary" icon="el-icon-plus" size="small" @click="addSection">新增标段</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="existingSections.length" class="existing-sections">
|
|
|
|
|
<div class="existing-sections-title">已拆分标段(不可修改)</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="(item, index) in existingSections"
|
|
|
|
|
:key="'exist-' + (item.id || index)"
|
|
|
|
|
class="section-block section-block--readonly"
|
|
|
|
|
>
|
|
|
|
|
<Tag color="default">已拆分 {{ index + 1 }}</Tag>
|
|
|
|
|
<el-form label-width="100px" style="margin-top: 8px;">
|
|
|
|
|
<el-form-item label="标段名称">
|
|
|
|
|
<div class="existing-sections-title">已拆分标段</div>
|
|
|
|
|
<el-form ref="existingForm" :model="{ existingSections }" label-width="100px">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(item, index) in existingSections"
|
|
|
|
|
:key="'exist-' + (item.id || index)"
|
|
|
|
|
class="section-block section-block--readonly"
|
|
|
|
|
>
|
|
|
|
|
<Tag color="primary">标段{{ displaySectionNo(item) }}</Tag>
|
|
|
|
|
<el-form-item
|
|
|
|
|
label="标段序号"
|
|
|
|
|
:prop="'existingSections.' + index + '.section_no'"
|
|
|
|
|
:rules="isEditingSectionNo ? sectionNoRules(null, item.id) : []"
|
|
|
|
|
style="margin-top: 8px;"
|
|
|
|
|
>
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-if="isEditingSectionNo"
|
|
|
|
|
:min="1"
|
|
|
|
|
:precision="0"
|
|
|
|
|
:controls="false"
|
|
|
|
|
v-model="item.section_no"
|
|
|
|
|
style="width: 100%;"
|
|
|
|
|
@change="resortExistingSections"
|
|
|
|
|
></el-input-number>
|
|
|
|
|
<span v-else>{{ displaySectionNo(item) }}</span>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="项目名称">
|
|
|
|
|
<span>{{ item.section_name }}</span>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="标段金额">
|
|
|
|
|
@ -27,11 +51,13 @@
|
|
|
|
|
<span class="plan-link-money">{{ formatMoney(link.use_money) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="isContinueMode && existingSections.length" class="new-sections-title">新增标段</div>
|
|
|
|
|
<div v-if="form.sections.length" class="new-sections-title">
|
|
|
|
|
{{ isContinueMode && existingSections.length ? '新增标段' : '标段信息' }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-form :model="form" ref="dynamicValidateForm" label-width="100px">
|
|
|
|
|
<div
|
|
|
|
|
@ -40,15 +66,28 @@
|
|
|
|
|
class="section-block"
|
|
|
|
|
>
|
|
|
|
|
<div style="display: flex;justify-content: space-between;margin-bottom: 8px;">
|
|
|
|
|
<Tag color="primary">标段 {{ existingSections.length + index + 1 }}</Tag>
|
|
|
|
|
<Tag color="primary">标段{{ item.section_no || '新' }}</Tag>
|
|
|
|
|
<el-button size="mini" type="danger" @click.prevent="removeSection(index)">删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-form-item
|
|
|
|
|
label="标段名称"
|
|
|
|
|
label="标段序号"
|
|
|
|
|
:prop="'sections.' + index + '.section_no'"
|
|
|
|
|
:rules="sectionNoRules(index)"
|
|
|
|
|
>
|
|
|
|
|
<el-input-number
|
|
|
|
|
:min="1"
|
|
|
|
|
:precision="0"
|
|
|
|
|
:controls="false"
|
|
|
|
|
v-model="item.section_no"
|
|
|
|
|
style="width: 100%;"
|
|
|
|
|
></el-input-number>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item
|
|
|
|
|
label="项目名称"
|
|
|
|
|
:prop="'sections.' + index + '.section_name'"
|
|
|
|
|
:rules="{ required: true, message: '标段名称不能为空', trigger: 'blur' }"
|
|
|
|
|
:rules="{ required: true, message: '项目名称不能为空', trigger: 'blur' }"
|
|
|
|
|
>
|
|
|
|
|
<el-input v-model="item.section_name"></el-input>
|
|
|
|
|
<el-input v-model="item.section_name" placeholder="请输入项目名称"></el-input>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item
|
|
|
|
|
label="标段金额"
|
|
|
|
|
@ -79,15 +118,21 @@
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<Button @click="hide">取消</Button>
|
|
|
|
|
<Button type="primary" :loading="submitting" @click="submit">确定</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import { splitSections, detailContract, getSections } from "@/api/contract/contract";
|
|
|
|
|
import { splitSections, detailContract, getSections, saveSection } from "@/api/contract/contract";
|
|
|
|
|
|
|
|
|
|
function emptySection() {
|
|
|
|
|
function emptySection(sectionNo) {
|
|
|
|
|
return {
|
|
|
|
|
section_no: sectionNo,
|
|
|
|
|
section_name: '',
|
|
|
|
|
plan_price: undefined,
|
|
|
|
|
plan_links: []
|
|
|
|
|
@ -99,12 +144,14 @@ export default {
|
|
|
|
|
return {
|
|
|
|
|
id: "",
|
|
|
|
|
isShow: false,
|
|
|
|
|
submitting: false,
|
|
|
|
|
row: {},
|
|
|
|
|
sourcePlans: [],
|
|
|
|
|
existingSections: [],
|
|
|
|
|
isContinueMode: false,
|
|
|
|
|
isEditingSectionNo: false,
|
|
|
|
|
form: {
|
|
|
|
|
sections: [emptySection()]
|
|
|
|
|
sections: [emptySection(1)]
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
@ -123,6 +170,99 @@ export default {
|
|
|
|
|
hide() {
|
|
|
|
|
this.isShow = false;
|
|
|
|
|
},
|
|
|
|
|
toggleEditSectionNo() {
|
|
|
|
|
this.isEditingSectionNo = !this.isEditingSectionNo;
|
|
|
|
|
if (this.isEditingSectionNo) {
|
|
|
|
|
this.existingSections = this.fillMissingSectionNos(this.existingSections);
|
|
|
|
|
} else if (this.$refs.existingForm) {
|
|
|
|
|
this.$refs.existingForm.clearValidate();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
displaySectionNo(item) {
|
|
|
|
|
const no = Number(item.section_no);
|
|
|
|
|
return no > 0 ? no : '-';
|
|
|
|
|
},
|
|
|
|
|
fillMissingSectionNos(sections) {
|
|
|
|
|
const list = [...(sections || [])];
|
|
|
|
|
const used = new Set();
|
|
|
|
|
list.forEach(item => {
|
|
|
|
|
const no = Number(item.section_no);
|
|
|
|
|
if (no > 0) {
|
|
|
|
|
used.add(no);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
list.forEach(item => {
|
|
|
|
|
const no = Number(item.section_no);
|
|
|
|
|
if (no > 0) {
|
|
|
|
|
item.section_no = no;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let n = 1;
|
|
|
|
|
while (used.has(n)) {
|
|
|
|
|
n += 1;
|
|
|
|
|
}
|
|
|
|
|
item.section_no = n;
|
|
|
|
|
used.add(n);
|
|
|
|
|
});
|
|
|
|
|
return this.sortSections(list);
|
|
|
|
|
},
|
|
|
|
|
sortSections(list) {
|
|
|
|
|
return [...(list || [])].sort((a, b) => {
|
|
|
|
|
const na = Number(a.section_no) || 0;
|
|
|
|
|
const nb = Number(b.section_no) || 0;
|
|
|
|
|
if (na !== nb) return na - nb;
|
|
|
|
|
return (a.id || 0) - (b.id || 0);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
resortExistingSections() {
|
|
|
|
|
this.existingSections = this.sortSections(this.existingSections);
|
|
|
|
|
},
|
|
|
|
|
collectUsedSectionNos(excludeExistingId = null, excludeNewIndex = null) {
|
|
|
|
|
const nos = [];
|
|
|
|
|
this.existingSections.forEach(item => {
|
|
|
|
|
if (excludeExistingId && item.id === excludeExistingId) return;
|
|
|
|
|
if (item.section_no != null && item.section_no !== '') {
|
|
|
|
|
nos.push(Number(item.section_no));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.form.sections.forEach((item, index) => {
|
|
|
|
|
if (excludeNewIndex === index) return;
|
|
|
|
|
if (item.section_no != null && item.section_no !== '') {
|
|
|
|
|
nos.push(Number(item.section_no));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return nos;
|
|
|
|
|
},
|
|
|
|
|
suggestNextSectionNo() {
|
|
|
|
|
const used = new Set(this.collectUsedSectionNos());
|
|
|
|
|
let n = 1;
|
|
|
|
|
while (used.has(n)) {
|
|
|
|
|
n += 1;
|
|
|
|
|
}
|
|
|
|
|
return n;
|
|
|
|
|
},
|
|
|
|
|
sectionNoRules(newIndex, existingId = null) {
|
|
|
|
|
return [{
|
|
|
|
|
required: true,
|
|
|
|
|
message: '标段序号不能为空',
|
|
|
|
|
trigger: 'blur'
|
|
|
|
|
}, {
|
|
|
|
|
validator: (rule, value, callback) => {
|
|
|
|
|
const no = Number(value);
|
|
|
|
|
if (!no || no <= 0) {
|
|
|
|
|
callback(new Error('标段序号须为正整数'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const duplicated = this.collectUsedSectionNos(existingId, newIndex).includes(no);
|
|
|
|
|
if (duplicated) {
|
|
|
|
|
callback(new Error('标段序号不可重复'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
callback();
|
|
|
|
|
},
|
|
|
|
|
trigger: 'blur'
|
|
|
|
|
}];
|
|
|
|
|
},
|
|
|
|
|
formatMoney(value) {
|
|
|
|
|
if (value == null || value === '') return '-';
|
|
|
|
|
return Number(value).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
|
|
|
|
|
@ -132,16 +272,19 @@ export default {
|
|
|
|
|
return this.formatPlanLabel(plan) || `计划#${link.plan_id}`;
|
|
|
|
|
},
|
|
|
|
|
addSection() {
|
|
|
|
|
const sec = emptySection();
|
|
|
|
|
const sec = emptySection(this.suggestNextSectionNo());
|
|
|
|
|
this.initPlanLinks(sec);
|
|
|
|
|
this.form.sections.push(sec);
|
|
|
|
|
},
|
|
|
|
|
removeSection(index) {
|
|
|
|
|
if (this.form.sections.length <= 1) {
|
|
|
|
|
if (this.form.sections.length <= 1 && !this.isContinueMode) {
|
|
|
|
|
this.$message.warning('至少保留一个标段');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.form.sections.splice(index, 1);
|
|
|
|
|
if (!this.form.sections.length && this.isContinueMode) {
|
|
|
|
|
this.form.sections = [];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
syncPlanLinkMoney(item) {
|
|
|
|
|
if (item.plan_links.length === 1 && item.plan_price != null) {
|
|
|
|
|
@ -187,46 +330,112 @@ export default {
|
|
|
|
|
this.row = await detailContract({ id: this.id });
|
|
|
|
|
this.sourcePlans = this.buildSourcePlans(this.row);
|
|
|
|
|
this.isContinueMode = !!(this.row.split_status || this.row.section_count > 0);
|
|
|
|
|
this.isEditingSectionNo = false;
|
|
|
|
|
this.existingSections = [];
|
|
|
|
|
if (this.isContinueMode) {
|
|
|
|
|
const res = await getSections({ contract_id: this.id });
|
|
|
|
|
this.existingSections = (res.children || []).map(child => ({
|
|
|
|
|
this.existingSections = this.fillMissingSectionNos(this.sortSections((res.children || []).map(child => ({
|
|
|
|
|
id: child.id,
|
|
|
|
|
section_name: child.section_name,
|
|
|
|
|
section_no: child.section_no != null && child.section_no !== '' ? Number(child.section_no) : null,
|
|
|
|
|
section_name: child.name || child.section_name,
|
|
|
|
|
plan_price: child.plan_price,
|
|
|
|
|
plan_links: child.plan_link || child.planLink || []
|
|
|
|
|
plan_links: child.plan_link || child.planLink || [],
|
|
|
|
|
originSectionNo: child.section_no != null && child.section_no !== '' ? Number(child.section_no) : null
|
|
|
|
|
}))));
|
|
|
|
|
}
|
|
|
|
|
const nextNo = this.suggestNextSectionNo();
|
|
|
|
|
this.form.sections = this.isContinueMode ? [] : [emptySection(nextNo)];
|
|
|
|
|
if (this.form.sections.length) {
|
|
|
|
|
this.initPlanLinks(this.form.sections[0]);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
validateNewSectionPlans(sections) {
|
|
|
|
|
for (const sec of sections) {
|
|
|
|
|
if (sec.plan_links && sec.plan_links.length) {
|
|
|
|
|
const sum = sec.plan_links.reduce((t, l) => t + Number(l.use_money || 0), 0);
|
|
|
|
|
if (Math.abs(sum - Number(sec.plan_price || 0)) > 0.009) {
|
|
|
|
|
this.$message.error(`标段「${sec.section_name}」占用预算金额须等于标段金额`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
validateAllForms() {
|
|
|
|
|
const tasks = [];
|
|
|
|
|
if (this.form.sections.length && this.$refs.dynamicValidateForm) {
|
|
|
|
|
tasks.push(new Promise(resolve => {
|
|
|
|
|
this.$refs.dynamicValidateForm.validate(valid => resolve(valid));
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
this.form.sections = [emptySection()];
|
|
|
|
|
this.initPlanLinks(this.form.sections[0]);
|
|
|
|
|
if (
|
|
|
|
|
this.isEditingSectionNo &&
|
|
|
|
|
this.$refs.existingForm &&
|
|
|
|
|
this.existingSections.length
|
|
|
|
|
) {
|
|
|
|
|
tasks.push(new Promise(resolve => {
|
|
|
|
|
this.$refs.existingForm.validate(valid => resolve(valid));
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
if (!tasks.length) {
|
|
|
|
|
return Promise.resolve(true);
|
|
|
|
|
}
|
|
|
|
|
return Promise.all(tasks).then(results => results.every(Boolean));
|
|
|
|
|
},
|
|
|
|
|
submit() {
|
|
|
|
|
this.$refs.dynamicValidateForm.validate(valid => {
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
getSectionsToUpdate() {
|
|
|
|
|
if (!this.isEditingSectionNo) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
return this.existingSections.filter(item => {
|
|
|
|
|
const current = Number(item.section_no);
|
|
|
|
|
const origin = item.originSectionNo;
|
|
|
|
|
return origin == null || origin <= 0 || current !== origin;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
async submit() {
|
|
|
|
|
this.submitting = true;
|
|
|
|
|
try {
|
|
|
|
|
const valid = await this.validateAllForms();
|
|
|
|
|
if (!valid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const sec of this.form.sections) {
|
|
|
|
|
if (sec.plan_links && sec.plan_links.length) {
|
|
|
|
|
const sum = sec.plan_links.reduce((t, l) => t + Number(l.use_money || 0), 0);
|
|
|
|
|
if (Math.abs(sum - Number(sec.plan_price || 0)) > 0.009) {
|
|
|
|
|
this.$message.error(`标段「${sec.section_name}」占用预算金额须等于标段金额`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const newSections = [...this.form.sections];
|
|
|
|
|
const sectionsToUpdate = this.getSectionsToUpdate();
|
|
|
|
|
|
|
|
|
|
if (!newSections.length && !sectionsToUpdate.length) {
|
|
|
|
|
this.$message.warning('请至少新增一个标段,或点击「编辑标段」修改标段序号');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (newSections.length && !this.validateNewSectionPlans(newSections)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
splitSections({
|
|
|
|
|
contract_id: this.id,
|
|
|
|
|
sections: this.form.sections.map(sec => ({
|
|
|
|
|
section_name: sec.section_name,
|
|
|
|
|
plan_price: sec.plan_price,
|
|
|
|
|
plan_links: (sec.plan_links || []).filter(l => l.plan_id && l.use_money != null)
|
|
|
|
|
}))
|
|
|
|
|
}).then(() => {
|
|
|
|
|
this.$message.success(this.isContinueMode ? '追加标段成功' : '拆分成功');
|
|
|
|
|
this.hide();
|
|
|
|
|
this.$emit('refresh');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
for (const item of sectionsToUpdate) {
|
|
|
|
|
await saveSection({
|
|
|
|
|
id: item.id,
|
|
|
|
|
section_no: item.section_no
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (newSections.length) {
|
|
|
|
|
await splitSections({
|
|
|
|
|
contract_id: this.id,
|
|
|
|
|
sections: newSections.map(sec => ({
|
|
|
|
|
section_no: sec.section_no,
|
|
|
|
|
section_name: sec.section_name,
|
|
|
|
|
plan_price: sec.plan_price,
|
|
|
|
|
plan_links: (sec.plan_links || []).filter(l => l.plan_id && l.use_money != null)
|
|
|
|
|
}))
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
this.$message.success(this.isContinueMode ? '保存成功' : '拆分成功');
|
|
|
|
|
this.hide();
|
|
|
|
|
this.$emit('refresh');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.$message.error(error?.message || '保存失败');
|
|
|
|
|
} finally {
|
|
|
|
|
this.submitting = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
@ -234,10 +443,12 @@ export default {
|
|
|
|
|
if (newVal) {
|
|
|
|
|
this.getDetail();
|
|
|
|
|
} else {
|
|
|
|
|
this.form.sections = [emptySection()];
|
|
|
|
|
this.form.sections = [emptySection(1)];
|
|
|
|
|
this.existingSections = [];
|
|
|
|
|
this.sourcePlans = [];
|
|
|
|
|
this.isContinueMode = false;
|
|
|
|
|
this.isEditingSectionNo = false;
|
|
|
|
|
this.submitting = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
id() {
|
|
|
|
|
@ -250,6 +461,23 @@ export default {
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.modal-toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
.modal-toolbar-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.modal-footer {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
text-align: right;
|
|
|
|
|
button + button {
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.existing-sections-title,
|
|
|
|
|
.new-sections-title {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|