标段序号

master
lion 2 weeks ago
parent e4f7bf3d0e
commit b779395733

@ -1,21 +1,45 @@
<template> <template>
<div> <div>
<Modal width="720" v-model="isShow" :title="modalTitle" @on-ok="submit"> <Modal width="720" v-model="isShow" :title="modalTitle" :footer-hide="true">
<div style="margin-bottom: 10px;display: flex;justify-content: space-between;align-items: center;"> <div class="modal-toolbar">
<span>源项目{{ row.name }}</span> <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>
<div v-if="existingSections.length" class="existing-sections"> <div v-if="existingSections.length" class="existing-sections">
<div class="existing-sections-title">已拆分标段不可修改</div> <div class="existing-sections-title">已拆分标段</div>
<div <el-form ref="existingForm" :model="{ existingSections }" label-width="100px">
v-for="(item, index) in existingSections" <div
:key="'exist-' + (item.id || index)" v-for="(item, index) in existingSections"
class="section-block section-block--readonly" :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;"> <Tag color="primary">标段{{ displaySectionNo(item) }}</Tag>
<el-form-item label="标段名称"> <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> <span>{{ item.section_name }}</span>
</el-form-item> </el-form-item>
<el-form-item label="标段金额"> <el-form-item label="标段金额">
@ -27,11 +51,13 @@
<span class="plan-link-money">{{ formatMoney(link.use_money) }}</span> <span class="plan-link-money">{{ formatMoney(link.use_money) }}</span>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </div>
</div> </el-form>
</div> </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"> <el-form :model="form" ref="dynamicValidateForm" label-width="100px">
<div <div
@ -40,15 +66,28 @@
class="section-block" class="section-block"
> >
<div style="display: flex;justify-content: space-between;margin-bottom: 8px;"> <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> <el-button size="mini" type="danger" @click.prevent="removeSection(index)">删除</el-button>
</div> </div>
<el-form-item <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'" :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>
<el-form-item <el-form-item
label="标段金额" label="标段金额"
@ -79,15 +118,21 @@
</el-form-item> </el-form-item>
</div> </div>
</el-form> </el-form>
<div class="modal-footer">
<Button @click="hide"></Button>
<Button type="primary" :loading="submitting" @click="submit"></Button>
</div>
</Modal> </Modal>
</div> </div>
</template> </template>
<script> <script>
import { splitSections, detailContract, getSections } from "@/api/contract/contract"; import { splitSections, detailContract, getSections, saveSection } from "@/api/contract/contract";
function emptySection() { function emptySection(sectionNo) {
return { return {
section_no: sectionNo,
section_name: '', section_name: '',
plan_price: undefined, plan_price: undefined,
plan_links: [] plan_links: []
@ -99,12 +144,14 @@ export default {
return { return {
id: "", id: "",
isShow: false, isShow: false,
submitting: false,
row: {}, row: {},
sourcePlans: [], sourcePlans: [],
existingSections: [], existingSections: [],
isContinueMode: false, isContinueMode: false,
isEditingSectionNo: false,
form: { form: {
sections: [emptySection()] sections: [emptySection(1)]
} }
}; };
}, },
@ -123,6 +170,99 @@ export default {
hide() { hide() {
this.isShow = false; 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) { formatMoney(value) {
if (value == null || value === '') return '-'; if (value == null || value === '') return '-';
return Number(value).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,'); return Number(value).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
@ -132,16 +272,19 @@ export default {
return this.formatPlanLabel(plan) || `计划#${link.plan_id}`; return this.formatPlanLabel(plan) || `计划#${link.plan_id}`;
}, },
addSection() { addSection() {
const sec = emptySection(); const sec = emptySection(this.suggestNextSectionNo());
this.initPlanLinks(sec); this.initPlanLinks(sec);
this.form.sections.push(sec); this.form.sections.push(sec);
}, },
removeSection(index) { removeSection(index) {
if (this.form.sections.length <= 1) { if (this.form.sections.length <= 1 && !this.isContinueMode) {
this.$message.warning('至少保留一个标段'); this.$message.warning('至少保留一个标段');
return; return;
} }
this.form.sections.splice(index, 1); this.form.sections.splice(index, 1);
if (!this.form.sections.length && this.isContinueMode) {
this.form.sections = [];
}
}, },
syncPlanLinkMoney(item) { syncPlanLinkMoney(item) {
if (item.plan_links.length === 1 && item.plan_price != null) { if (item.plan_links.length === 1 && item.plan_price != null) {
@ -187,46 +330,112 @@ export default {
this.row = await detailContract({ id: this.id }); this.row = await detailContract({ id: this.id });
this.sourcePlans = this.buildSourcePlans(this.row); this.sourcePlans = this.buildSourcePlans(this.row);
this.isContinueMode = !!(this.row.split_status || this.row.section_count > 0); this.isContinueMode = !!(this.row.split_status || this.row.section_count > 0);
this.isEditingSectionNo = false;
this.existingSections = []; this.existingSections = [];
if (this.isContinueMode) { if (this.isContinueMode) {
const res = await getSections({ contract_id: this.id }); 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, 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_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()]; if (
this.initPlanLinks(this.form.sections[0]); 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() { getSectionsToUpdate() {
this.$refs.dynamicValidateForm.validate(valid => { if (!this.isEditingSectionNo) {
if (!valid) return; 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) { const newSections = [...this.form.sections];
if (sec.plan_links && sec.plan_links.length) { const sectionsToUpdate = this.getSectionsToUpdate();
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) { if (!newSections.length && !sectionsToUpdate.length) {
this.$message.error(`标段「${sec.section_name}」占用预算金额须等于标段金额`); this.$message.warning('请至少新增一个标段,或点击「编辑标段」修改标段序号');
return; return;
} }
} if (newSections.length && !this.validateNewSectionPlans(newSections)) {
return;
} }
splitSections({ for (const item of sectionsToUpdate) {
contract_id: this.id, await saveSection({
sections: this.form.sections.map(sec => ({ id: item.id,
section_name: sec.section_name, section_no: item.section_no
plan_price: sec.plan_price, });
plan_links: (sec.plan_links || []).filter(l => l.plan_id && l.use_money != null) }
})) if (newSections.length) {
}).then(() => { await splitSections({
this.$message.success(this.isContinueMode ? '追加标段成功' : '拆分成功'); contract_id: this.id,
this.hide(); sections: newSections.map(sec => ({
this.$emit('refresh'); 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: { watch: {
@ -234,10 +443,12 @@ export default {
if (newVal) { if (newVal) {
this.getDetail(); this.getDetail();
} else { } else {
this.form.sections = [emptySection()]; this.form.sections = [emptySection(1)];
this.existingSections = []; this.existingSections = [];
this.sourcePlans = []; this.sourcePlans = [];
this.isContinueMode = false; this.isContinueMode = false;
this.isEditingSectionNo = false;
this.submitting = false;
} }
}, },
id() { id() {
@ -250,6 +461,23 @@ export default {
</script> </script>
<style scoped lang="scss"> <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, .existing-sections-title,
.new-sections-title { .new-sections-title {
font-weight: 600; font-weight: 600;

@ -2515,17 +2515,16 @@ export default {
this.list.splice(sourceIndex + 1, removeCount) this.list.splice(sourceIndex + 1, removeCount)
} }
}, },
formatSectionLabel(index) { formatSectionLabel(sectionNo) {
const n = Number(index) + 1 const n = Number(sectionNo)
const cn = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'] if (n > 0) {
if (n >= 1 && n <= 10) { return `标段${n}`
return `标段${cn[n - 1]}`
} }
return `标段${n}` return '标段'
}, },
renderProjectNameCell(row) { renderProjectNameCell(row) {
const displayName = row.is_split_child const displayName = row.is_split_child
? (row.section_name || row.name) ? (row.name || row.section_name)
: row.name : row.name
const expired = row.type === 1 && row.end_date && const expired = row.type === 1 && row.end_date &&
(this.$moment().valueOf() - this.$moment(row.end_date).valueOf()) > 0 (this.$moment().valueOf() - this.$moment(row.end_date).valueOf()) > 0
@ -2547,7 +2546,7 @@ export default {
} }
if (row.is_split_child) { if (row.is_split_child) {
const sectionLabel = this.formatSectionLabel(row._splitSectionIndex ?? 0) const sectionLabel = this.formatSectionLabel(row.section_no || row._displaySectionNo)
return ( return (
<span style="display:inline-flex;align-items:center;padding-left:12px;"> <span style="display:inline-flex;align-items:center;padding-left:12px;">
<Tag color="primary" size="small" style="margin-right:8px;">{sectionLabel}</Tag> <Tag color="primary" size="small" style="margin-right:8px;">{sectionLabel}</Tag>
@ -2569,18 +2568,39 @@ export default {
}) })
}).catch(() => {}) }).catch(() => {})
}, },
sortSplitChildren(children) {
const sorted = [...(children || [])].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)
})
const used = new Set()
return sorted.map(child => {
const no = Number(child.section_no)
if (no > 0) {
used.add(no)
return child
}
let n = 1
while (used.has(n)) {
n += 1
}
used.add(n)
return { ...child, _displaySectionNo: n }
})
},
buildDisplayList(data) { buildDisplayList(data) {
const rows = [] const rows = []
;(data || []).forEach(source => { ;(data || []).forEach(source => {
const children = source.split_children || source.splitChildren || [] const children = this.sortSplitChildren(source.split_children || source.splitChildren || [])
const hasChildren = children.length > 0 const hasChildren = children.length > 0
rows.push({ ...source, _hasSplitChildren: hasChildren }) rows.push({ ...source, _hasSplitChildren: hasChildren })
if (hasChildren && this.isSplitGroupExpanded(source.id)) { if (hasChildren && this.isSplitGroupExpanded(source.id)) {
children.forEach((child, index) => { children.forEach(child => {
rows.push(this.enrichSplitChildRow({ rows.push(this.enrichSplitChildRow({
...child, ...child,
_splitParentId: source.id, _splitParentId: source.id
_splitSectionIndex: index
}, source)) }, source))
}) })
} }
@ -2592,12 +2612,11 @@ export default {
if (this.splitChildrenRowsCache[sourceId]) { if (this.splitChildrenRowsCache[sourceId]) {
return this.splitChildrenRowsCache[sourceId] return this.splitChildrenRowsCache[sourceId]
} }
const children = source.split_children || source.splitChildren || [] const children = this.sortSplitChildren(source.split_children || source.splitChildren || [])
const rows = children.map((child, index) => { const rows = children.map(child => {
const row = this.enrichSplitChildRow({ const row = this.enrichSplitChildRow({
...child, ...child,
_splitParentId: sourceId, _splitParentId: sourceId
_splitSectionIndex: index
}, source) }, source)
this.handleContractFlow(row) this.handleContractFlow(row)
return row return row
@ -2609,7 +2628,7 @@ export default {
this.splitChildrenRowsCache = {} this.splitChildrenRowsCache = {}
const rows = [] const rows = []
;(data || []).forEach(source => { ;(data || []).forEach(source => {
const children = source.split_children || source.splitChildren || [] const children = this.sortSplitChildren(source.split_children || source.splitChildren || [])
const hasChildren = children.length > 0 const hasChildren = children.length > 0
const sourceRow = { ...source, _hasSplitChildren: hasChildren } const sourceRow = { ...source, _hasSplitChildren: hasChildren }
this.handleContractFlow(sourceRow) this.handleContractFlow(sourceRow)

Loading…
Cancel
Save