标段序号

master
lion 4 days ago
parent e4f7bf3d0e
commit b779395733

@ -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;

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

Loading…
Cancel
Save