Files
tjt_czjs_ui/src/views/tjt/output/PlanningOutputForm.vue

1299 lines
43 KiB
Vue
Raw Normal View History

2026-04-29 15:44:00 +08:00
<template>
<Dialog v-model="dialogVisible" title="编辑测算参数" width="92%">
2026-04-17 18:17:42 +08:00
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="150px"
>
2026-04-25 18:10:45 +08:00
<el-divider content-position="left">规划信息</el-divider>
2026-04-17 18:17:42 +08:00
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="归属类型">
2026-04-29 15:44:00 +08:00
<el-input :model-value="getOwnershipTypeLabel(formData.ownershipType)" disabled />
2026-04-17 18:17:42 +08:00
</el-form-item>
</el-col>
<el-col :span="8">
2026-04-25 18:10:45 +08:00
<el-form-item label="产值计算方式" prop="calculationMethod">
<el-select
v-model="formData.calculationMethod"
class="!w-1/1"
placeholder="请选择产值计算方式"
>
<el-option
2026-04-29 15:44:00 +08:00
v-for="item in CALCULATION_METHOD_OPTIONS"
2026-04-25 18:10:45 +08:00
:key="String(item.value)"
:label="item.label"
:value="item.value"
/>
</el-select>
2026-04-17 18:17:42 +08:00
</el-form-item>
</el-col>
<el-col :span="8">
2026-05-08 17:38:50 +08:00
<el-form-item label="项目任务包">
2026-04-17 18:17:42 +08:00
<el-input :model-value="formData.planningContent" disabled />
</el-form-item>
</el-col>
</el-row>
2026-04-25 18:10:45 +08:00
2026-04-17 18:17:42 +08:00
<el-row :gutter="16">
<el-col :span="8">
2026-05-08 17:38:50 +08:00
<el-form-item label="合同产值数量">
<el-input :model-value="formatQuantityText(formData.contractValueQuantity)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合同产值单价">
<el-input :model-value="formatQuantityText(formData.contractValueUnitPrice)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="分项合同产值(元)">
2026-04-17 18:17:42 +08:00
<el-input :model-value="formatAmountText(formData.planningAmount)" disabled />
</el-form-item>
</el-col>
2026-05-08 17:38:50 +08:00
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="增值税率(%)">
<el-input :model-value="formatPercentText(formData.vatRate)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="增值税(元)">
<el-input :model-value="formatAmountText(formData.vatAmount)" disabled />
</el-form-item>
</el-col>
2026-04-17 18:17:42 +08:00
<el-col :span="8">
2026-04-25 18:10:45 +08:00
<el-form-item label="管理费率(%)">
2026-04-17 18:17:42 +08:00
<el-input :model-value="formatPercentText(formData.managementFeeRate)" disabled />
</el-form-item>
</el-col>
2026-05-08 17:38:50 +08:00
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="管理费(元)">
<el-input :model-value="formatAmountText(formData.managementFee)" disabled />
</el-form-item>
</el-col>
2026-04-17 18:17:42 +08:00
<el-col :span="8">
2026-05-08 17:38:50 +08:00
<el-form-item label="项目预算产值(元)">
<el-input :model-value="formatAmountText(formData.projectBudgetOutputValue)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="意向实施团队">
2026-04-17 18:17:42 +08:00
<el-input :model-value="formData.implementationTeam || '-'" disabled />
</el-form-item>
</el-col>
</el-row>
2026-04-25 18:10:45 +08:00
2026-04-17 18:17:42 +08:00
<el-divider content-position="left">测算参数</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="开始年度" prop="planningStartYear">
<el-date-picker
v-model="planningStartYearValue"
class="!w-1/1"
placeholder="请选择开始年度"
type="year"
value-format="YYYY"
/>
</el-form-item>
</el-col>
<el-col :span="8">
2026-04-25 18:10:45 +08:00
<el-form-item label="工程总面积(m²)" prop="planningArea">
2026-04-29 15:44:00 +08:00
<el-input
v-if="showGuideDetailSection"
:model-value="formatAmountText(guideDetailSummary.designArea)"
disabled
/>
2026-04-17 18:17:42 +08:00
<el-input-number
2026-04-29 15:44:00 +08:00
v-else
2026-04-17 18:17:42 +08:00
v-model="formData.planningArea"
:min="0"
:precision="2"
:step="100"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="8">
2026-04-25 18:10:45 +08:00
<el-form-item label="合同单价(元/m²)">
2026-04-17 18:17:42 +08:00
<el-input :model-value="contractUnitPricePreview" disabled />
</el-form-item>
</el-col>
</el-row>
2026-04-25 18:10:45 +08:00
2026-04-17 18:17:42 +08:00
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="设计阶段" prop="designStage">
2026-04-29 15:44:00 +08:00
<el-select v-model="formData.designStage" class="!w-1/1" placeholder="请选择设计阶段">
2026-04-17 18:17:42 +08:00
<el-option
2026-04-29 15:44:00 +08:00
v-for="item in DESIGN_STAGE_OPTIONS"
2026-04-25 18:10:45 +08:00
:key="String(item.value)"
2026-04-17 18:17:42 +08:00
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="本次设计阶段比例(%)" prop="currentDesignStageRatio">
<el-input-number
v-model="currentDesignStageRatioPercent"
:min="0"
:precision="2"
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
2026-04-29 15:44:00 +08:00
</el-row>
<el-row :gutter="16">
2026-04-17 18:17:42 +08:00
<el-col :span="8">
<el-form-item label="审核审定是否外包" prop="reviewOutsourceFlag">
<el-switch
v-model="formData.reviewOutsourceFlag"
active-text="是"
inactive-text="否"
@change="handleReviewOutsourceFlagChange"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="审核审定占比(%)" prop="reviewOutsourceRatio">
<el-input-number
v-model="reviewOutsourceRatioPercent"
:min="0"
:precision="2"
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col v-if="showCalculationRatioField" :span="8">
<el-form-item :label="`${calculationRatioLabel}(%)`" prop="calculationRatio">
<el-input-number
v-model="calculationRatioPercent"
:min="0"
:precision="2"
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
2026-04-29 15:44:00 +08:00
</el-row>
<el-row :gutter="16">
<el-col :span="8">
2026-04-17 18:17:42 +08:00
<el-form-item label="总分配(%)" prop="totalDistributionAmount">
<el-input-number
v-model="totalDistributionAmountPercent"
:min="0"
:precision="2"
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
</el-row>
2026-04-29 15:44:00 +08:00
<template v-if="showParentMajorFactorFields">
2026-04-17 18:17:42 +08:00
<el-divider content-position="left">专业所测算参数</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="楼栋数/户型数" prop="buildingOrUnitCount">
<el-input-number
v-model="formData.buildingOrUnitCount"
:min="0"
:precision="0"
:step="1"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="套图系数" prop="drawingSetFactor">
<el-input-number
v-model="formData.drawingSetFactor"
:min="0"
2026-05-09 11:11:56 +08:00
:precision="2"
2026-04-17 18:17:42 +08:00
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="规模系数" prop="scaleFactor">
<el-input-number
v-model="formData.scaleFactor"
:min="0"
2026-05-09 11:11:56 +08:00
:precision="2"
2026-04-17 18:17:42 +08:00
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="修改系数" prop="modificationFactor">
<el-input-number
v-model="formData.modificationFactor"
:min="0"
2026-05-09 11:11:56 +08:00
:precision="2"
2026-04-17 18:17:42 +08:00
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="复杂系数/复杂等级(%)" prop="complexityFactor">
<el-input-number
v-model="complexityFactorPercent"
:min="0"
:precision="2"
:step="0.01"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
</el-row>
</template>
2026-04-29 15:44:00 +08:00
<!-- 优化后的指导价法明细块 -->
<template v-if="showGuideDetailSection">
<el-divider content-position="left">指导价法明细</el-divider>
<div class="mb-12px flex items-center justify-between gap-12px">
<el-button plain type="primary" @click="addGuideDetailRow">
<template #icon><i class="el-icon-plus"></i></template>新增明细
</el-button>
</div>
<el-table :data="guideDetails" border max-height="460" class="optimized-table">
<!-- 1. 冻结核心上下文列 -->
<el-table-column align="center" label="序号" width="65" fixed="left">
<template #default="{ row }">
<el-input-number
v-model="row.sortNo"
:min="1"
:precision="0"
:controls="false"
class="!w-1/1"
/>
</template>
</el-table-column>
<el-table-column align="center" label="设计部位" min-width="120" fixed="left">
<template #default="{ row }">
<el-select v-model="row.designPart" class="!w-1/1" placeholder="请选择">
<el-option
v-for="item in DESIGN_PART_OPTIONS"
:key="String(item.value)"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column align="center" label="建筑类型" min-width="140" fixed="left">
<template #default="{ row }">
2026-05-09 11:11:56 +08:00
<el-tooltip
:content="row.buildingType"
:disabled="!hasValue(row.buildingType)"
placement="top"
>
<el-input v-model="row.buildingType" maxlength="100" placeholder="建筑类型" />
</el-tooltip>
2026-04-29 15:44:00 +08:00
</template>
</el-table-column>
<!-- 2. 取消 controls 的核心数值列 -->
<el-table-column align="center" label="指导单价(元)" min-width="110">
<template #default="{ row }">
2026-05-09 11:11:56 +08:00
<el-tooltip
:content="formatAmountText(row.internalGuidanceUnitPrice)"
:disabled="!hasValue(row.internalGuidanceUnitPrice)"
placement="top"
>
<el-input-number
v-model="row.internalGuidanceUnitPrice"
:min="0"
:precision="2"
:controls="false"
class="!w-1/1"
/>
</el-tooltip>
2026-04-29 15:44:00 +08:00
</template>
</el-table-column>
<el-table-column align="center" label="设计面积(m²)" min-width="110">
<template #default="{ row }">
2026-05-09 11:11:56 +08:00
<el-tooltip
:content="formatAmountText(row.designArea)"
:disabled="!hasValue(row.designArea)"
placement="top"
>
<el-input-number
v-model="row.designArea"
:min="0"
:precision="2"
:controls="false"
class="!w-1/1"
/>
</el-tooltip>
2026-04-29 15:44:00 +08:00
</template>
</el-table-column>
<el-table-column align="center" label="楼栋/户型数" min-width="100">
<template #default="{ row }">
<el-input-number
v-model="row.buildingOrUnitCount"
:min="0"
:precision="0"
:controls="false"
class="!w-1/1"
/>
</template>
</el-table-column>
<!-- 3. 多级表头折叠系数配置 -->
<el-table-column label="调整系数配置" align="center">
<el-table-column align="center" label="套图" min-width="85">
<template #default="{ row }">
2026-05-09 11:11:56 +08:00
<el-input-number v-model="row.drawingSetFactor" :min="0" :precision="2" :controls="false" class="!w-1/1" />
2026-04-29 15:44:00 +08:00
</template>
</el-table-column>
<el-table-column align="center" label="规模" min-width="85">
<template #default="{ row }">
2026-05-09 11:11:56 +08:00
<el-input-number v-model="row.scaleFactor" :min="0" :precision="2" :controls="false" class="!w-1/1" />
2026-04-29 15:44:00 +08:00
</template>
</el-table-column>
<el-table-column align="center" label="修改" min-width="85">
<template #default="{ row }">
2026-05-09 11:11:56 +08:00
<el-input-number v-model="row.modificationFactor" :min="0" :precision="2" :controls="false" class="!w-1/1" />
2026-04-29 15:44:00 +08:00
</template>
</el-table-column>
<el-table-column align="center" label="复杂(%)" min-width="90">
<template #default="{ row }">
<el-input-number
:model-value="toPercentValue(row.complexityFactor)"
:min="0"
:precision="2"
:controls="false"
class="!w-1/1"
@update:model-value="setGuideDetailPercentValue(row, 'complexityFactor', $event)"
/>
</template>
</el-table-column>
<el-table-column align="center" label="合计" min-width="80">
<template #default="{ row }">
<span class="text-gray-500">{{ formatFactorText(getGuideDetailTotalAdjustmentFactor(row)) }}</span>
</template>
</el-table-column>
</el-table-column>
<el-table-column align="center" label="设计占比(%)" min-width="100">
<template #default="{ row }">
<el-input-number
:model-value="toPercentValue(row.designRatio)"
:min="0"
:precision="2"
:controls="false"
class="!w-1/1"
@update:model-value="setGuideDetailPercentValue(row, 'designRatio', $event)"
/>
</template>
</el-table-column>
<!-- 4. 结果列靠右显示用浅色背景区分 -->
<el-table-column align="right" label="考核面积(m²)" min-width="110" class-name="bg-gray-50">
<template #default="{ row }">
<span class="font-bold">{{ formatAmountText(getGuideDetailAssessmentArea(row)) }}</span>
</template>
</el-table-column>
<el-table-column align="right" label="考核产值(元)" min-width="120" class-name="bg-gray-50">
<template #default="{ row }">
<span class="font-bold text-primary">{{ formatAmountText(getGuideDetailAssessmentOutputValue(row)) }}</span>
</template>
</el-table-column>
2026-05-09 11:11:56 +08:00
<el-table-column align="left" label="备注" min-width="260">
2026-04-29 15:44:00 +08:00
<template #default="{ row }">
2026-05-09 11:11:56 +08:00
<div class="remark-cell">
<el-tooltip
:disabled="!hasGuideDetailRemark(row.remark)"
effect="dark"
placement="top-start"
popper-class="guide-remark-tooltip"
>
<template #content>
<div class="remark-tooltip-content">{{ row.remark }}</div>
</template>
<div
class="remark-preview"
:class="{ 'is-empty': !hasGuideDetailRemark(row.remark) }"
>
{{ formatGuideDetailRemarkPreview(row.remark) }}
</div>
</el-tooltip>
<el-button
class="remark-edit-button !h-auto !p-0"
link
type="primary"
@click="openGuideDetailRemarkDialog(row)"
>
{{ hasGuideDetailRemark(row.remark) ? '编辑' : '填写' }}
</el-button>
</div>
2026-04-29 15:44:00 +08:00
</template>
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="70">
<template #default="{ $index }">
<el-button link type="danger" @click="removeGuideDetailRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row :gutter="16" class="mt-16px">
<el-col :span="8">
<el-form-item label="汇总设计面积(m²)">
<el-input :model-value="formatAmountText(guideDetailSummary.designArea)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="汇总考核面积(m²)">
<el-input :model-value="formatAmountText(guideDetailSummary.assessmentArea)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="汇总考核产值(元)">
<el-input
:model-value="formatAmountText(guideDetailSummary.assessmentOutputValue)"
disabled
2026-04-17 18:17:42 +08:00
/>
</el-form-item>
</el-col>
</el-row>
</template>
<template v-if="showVirtualOutputSection">
<el-divider content-position="left">虚拟产值法参数</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="虚拟产值计算方式" prop="virtualCalculationMethod">
<el-select
v-model="formData.virtualCalculationMethod"
class="!w-1/1"
placeholder="请选择虚拟产值计算方式"
>
<el-option
2026-04-29 15:44:00 +08:00
v-for="item in VIRTUAL_CALCULATION_METHOD_OPTIONS"
2026-04-25 18:10:45 +08:00
:key="String(item.value)"
2026-04-17 18:17:42 +08:00
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="showWorkingDayFields" :span="8">
<el-form-item label="工日" prop="workingDayCount">
<el-input-number
v-model="formData.workingDayCount"
:min="0"
:precision="2"
:step="1"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
<el-col v-if="showWorkingDayFields" :span="8">
<el-form-item label="工日单价(元)" prop="workingDayUnitPrice">
<el-input-number
v-model="formData.workingDayUnitPrice"
:min="0"
:precision="2"
:step="100"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
</el-row>
2026-04-25 18:10:45 +08:00
<el-row v-if="showGuidanceUnitPriceField" :gutter="16">
2026-04-17 18:17:42 +08:00
<el-col :span="8">
2026-04-25 18:10:45 +08:00
<el-form-item label="指导单价(元/m²)" prop="guidanceUnitPrice">
2026-04-17 18:17:42 +08:00
<el-input-number
v-model="formData.guidanceUnitPrice"
:min="0"
:precision="2"
:step="1"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
2026-04-25 18:10:45 +08:00
</el-row>
<el-row v-if="showGuidanceTotalPriceField" :gutter="16">
2026-04-17 18:17:42 +08:00
<el-col :span="8">
<el-form-item label="指导总价(元)" prop="guidanceTotalPrice">
<el-input-number
v-model="formData.guidanceTotalPrice"
:min="0"
:precision="2"
:step="1000"
class="!w-1/1"
controls-position="right"
/>
</el-form-item>
</el-col>
</el-row>
</template>
</el-form>
2026-04-29 15:44:00 +08:00
2026-04-17 18:17:42 +08:00
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">保存</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</template>
</Dialog>
2026-05-09 11:11:56 +08:00
<Dialog v-model="remarkDialogVisible" title="指导价法明细备注" width="720">
<el-descriptions v-if="activeRemarkRow" :column="2" border class="mb-16px">
<el-descriptions-item label="序号">
{{ activeRemarkRow.sortNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="设计部位">
{{ activeRemarkRow.designPart || '-' }}
</el-descriptions-item>
<el-descriptions-item label="建筑类型">
{{ activeRemarkRow.buildingType || '-' }}
</el-descriptions-item>
<el-descriptions-item label="考核产值(元)">
{{ formatAmountText(getGuideDetailAssessmentOutputValue(activeRemarkRow)) }}
</el-descriptions-item>
</el-descriptions>
<el-input
v-model="remarkDraft"
:autosize="{ minRows: 8, maxRows: 14 }"
maxlength="500"
placeholder="请输入备注,例如计价说明、图纸版本说明、附加系数说明等"
show-word-limit
type="textarea"
/>
<template #footer>
<el-button type="primary" @click="saveGuideDetailRemark">确定</el-button>
<el-button @click="remarkDialogVisible = false">取消</el-button>
</template>
</Dialog>
2026-04-17 18:17:42 +08:00
</template>
<script lang="ts" setup>
2026-04-29 15:44:00 +08:00
// [原有的 Script 逻辑不变,直接粘贴原本的逻辑即可]
2026-04-17 18:17:42 +08:00
import type { FormRules } from 'element-plus'
import * as PlanningApi from '@/api/tjt/planning'
2026-04-29 15:44:00 +08:00
import * as PlanningGuideDetailApi from '@/api/tjt/planningGuideDetail'
2026-04-17 18:17:42 +08:00
import {
2026-04-25 18:10:45 +08:00
CALCULATION_METHOD_OPTIONS,
DEFAULT_WORKING_DAY_UNIT_PRICE,
2026-04-29 15:44:00 +08:00
DESIGN_PART_OPTIONS,
2026-04-17 18:17:42 +08:00
DESIGN_STAGE_OPTIONS,
VIRTUAL_CALCULATION_METHOD_OPTIONS,
formatAmountText,
formatPercentText,
fromPercentValue,
getCalculationRatioDefaultPercent,
getCalculationRatioLabel,
2026-04-29 15:44:00 +08:00
getOwnershipTypeLabel,
2026-04-17 18:17:42 +08:00
getReviewOutsourceDefaultPercent,
isComprehensiveOwnership,
isContractPriceMethod,
isGuidancePriceMethod,
isMajorOwnership,
isSubcontractOwnership,
isVirtualGuidanceMethod,
2026-04-25 18:10:45 +08:00
isVirtualGuidanceTotalPriceMethod,
2026-04-17 18:17:42 +08:00
isVirtualOutputMethod,
isWorkingDayMethod,
2026-04-25 18:10:45 +08:00
normalizeCalculationMethod,
normalizeDesignStage,
normalizeOwnershipType,
normalizeVirtualCalculationMethod,
2026-04-17 18:17:42 +08:00
toPercentValue
} from '@/views/tjt/shared/planning'
defineOptions({ name: 'TjtPlanningOutputForm' })
2026-04-29 15:44:00 +08:00
type GuideDetailRow = PlanningGuideDetailApi.ProjectPlanningGuideDetailVO
2026-04-25 18:10:45 +08:00
2026-04-17 18:17:42 +08:00
const message = useMessage()
const dialogVisible = ref(false)
const formLoading = ref(false)
const formRef = ref()
2026-04-29 15:44:00 +08:00
const createFormData = (): PlanningApi.ProjectPlanningVO => ({
id: undefined,
2026-04-17 18:17:42 +08:00
projectId: 0,
ownershipType: '',
calculationMethod: '',
2026-04-29 15:44:00 +08:00
planningContent: '',
planningAmount: undefined,
2026-05-08 17:38:50 +08:00
contractValueQuantity: undefined,
contractValueUnitPrice: undefined,
2026-04-29 15:44:00 +08:00
managementFeeRate: undefined,
managementFee: undefined,
2026-05-08 17:38:50 +08:00
vatRate: undefined,
vatAmount: undefined,
projectBudgetOutputValue: undefined,
2026-04-29 15:44:00 +08:00
implementationTeam: '',
planningStartYear: undefined,
planningArea: undefined,
designStage: undefined,
currentDesignStageRatio: undefined,
reviewOutsourceFlag: false,
reviewOutsourceRatio: undefined,
totalDistributionAmount: 1,
progressRemark: '',
allocatedAmount: undefined,
pendingAmount: undefined,
buildingOrUnitCount: undefined,
drawingSetFactor: undefined,
scaleFactor: undefined,
modificationFactor: undefined,
complexityFactor: undefined,
internalGuidanceUnitPrice: undefined,
virtualCalculationMethod: undefined,
workingDayCount: undefined,
workingDayUnitPrice: undefined,
guidanceUnitPrice: undefined,
guidanceTotalPrice: undefined,
calculationRatio: undefined,
contractUnitPrice: undefined,
totalAdjustmentFactor: undefined,
assessmentArea: undefined,
virtualOutputValue: undefined,
assessmentOutputValue: undefined,
createTime: undefined
2026-04-17 18:17:42 +08:00
})
2026-04-29 15:44:00 +08:00
const formData = ref<PlanningApi.ProjectPlanningVO>(createFormData())
const guideDetails = ref<GuideDetailRow[]>([])
2026-05-09 11:11:56 +08:00
const remarkDialogVisible = ref(false)
const activeRemarkRow = ref<GuideDetailRow>()
const remarkDraft = ref('')
2026-04-17 18:17:42 +08:00
const planningStartYearValue = computed({
2026-04-29 15:44:00 +08:00
get: () => (formData.value.planningStartYear ? String(formData.value.planningStartYear) : undefined),
2026-04-17 18:17:42 +08:00
set: (value?: string) => {
formData.value.planningStartYear = value ? Number(value) : undefined
}
})
const createPercentModel = (field: keyof PlanningApi.ProjectPlanningVO, digits = 2) =>
computed({
get: () => toPercentValue(formData.value[field] as number | undefined, digits),
set: (value) => {
formData.value[field] = fromPercentValue(value, 4) as never
}
})
const currentDesignStageRatioPercent = createPercentModel('currentDesignStageRatio')
const reviewOutsourceRatioPercent = createPercentModel('reviewOutsourceRatio')
const calculationRatioPercent = createPercentModel('calculationRatio')
const totalDistributionAmountPercent = createPercentModel('totalDistributionAmount')
const complexityFactorPercent = createPercentModel('complexityFactor')
const showCalculationRatioField = computed(
() =>
isComprehensiveOwnership(formData.value.ownershipType) ||
isSubcontractOwnership(formData.value.ownershipType)
)
2026-04-29 15:44:00 +08:00
const showGuideDetailSection = computed(
2026-04-17 18:17:42 +08:00
() =>
isMajorOwnership(formData.value.ownershipType) &&
2026-04-29 15:44:00 +08:00
isGuidancePriceMethod(formData.value.calculationMethod)
2026-04-17 18:17:42 +08:00
)
2026-04-29 15:44:00 +08:00
const showParentMajorFactorFields = computed(
2026-04-17 18:17:42 +08:00
() =>
isMajorOwnership(formData.value.ownershipType) &&
2026-04-29 15:44:00 +08:00
isContractPriceMethod(formData.value.calculationMethod)
2026-04-17 18:17:42 +08:00
)
2026-04-29 15:44:00 +08:00
2026-04-17 18:17:42 +08:00
const showVirtualOutputSection = computed(
() =>
isMajorOwnership(formData.value.ownershipType) &&
isVirtualOutputMethod(formData.value.calculationMethod)
)
2026-04-29 15:44:00 +08:00
2026-04-17 18:17:42 +08:00
const showWorkingDayFields = computed(() => isWorkingDayMethod(formData.value.virtualCalculationMethod))
2026-04-25 18:10:45 +08:00
const showGuidanceUnitPriceField = computed(
2026-04-17 18:17:42 +08:00
() => isVirtualGuidanceMethod(formData.value.virtualCalculationMethod)
)
2026-04-25 18:10:45 +08:00
const showGuidanceTotalPriceField = computed(
() => isVirtualGuidanceTotalPriceMethod(formData.value.virtualCalculationMethod)
2026-04-17 18:17:42 +08:00
)
2026-04-29 15:44:00 +08:00
const calculationRatioLabel = computed(() => getCalculationRatioLabel(formData.value.ownershipType))
const normalizeFormData = (data: PlanningApi.ProjectPlanningVO): PlanningApi.ProjectPlanningVO => ({
...createFormData(),
...data,
ownershipType: normalizeOwnershipType(data.ownershipType) || data.ownershipType || '',
calculationMethod: normalizeCalculationMethod(data.calculationMethod) || data.calculationMethod || '',
2026-05-08 17:38:50 +08:00
contractValueQuantity: data.contractValueQuantity ?? 1,
contractValueUnitPrice: data.contractValueUnitPrice ?? data.planningAmount,
vatRate: data.vatRate ?? 0.06,
2026-04-29 15:44:00 +08:00
designStage: normalizeDesignStage(data.designStage),
virtualCalculationMethod: normalizeVirtualCalculationMethod(data.virtualCalculationMethod),
reviewOutsourceFlag: data.reviewOutsourceFlag ?? false,
totalDistributionAmount: data.totalDistributionAmount ?? 1,
progressRemark: data.progressRemark ?? '',
workingDayUnitPrice:
data.workingDayUnitPrice ??
(isWorkingDayMethod(data.virtualCalculationMethod) ? DEFAULT_WORKING_DAY_UNIT_PRICE : undefined)
})
const createGuideDetailRow = (sortNo: number): GuideDetailRow => ({
planningId: formData.value.id || 0,
sortNo,
designPart: undefined,
buildingType: '',
designArea: undefined,
internalGuidanceUnitPrice: undefined,
buildingOrUnitCount: undefined,
drawingSetFactor: undefined,
scaleFactor: undefined,
modificationFactor: undefined,
complexityFactor: undefined,
totalAdjustmentFactor: undefined,
designRatio: undefined,
assessmentArea: undefined,
assessmentOutputValue: undefined,
remark: ''
})
const normalizeGuideDetailList = (
list: PlanningGuideDetailApi.ProjectPlanningGuideDetailVO[] | undefined
): GuideDetailRow[] =>
[...(list || [])]
.sort(
(a, b) =>
Number(a.sortNo ?? Number.MAX_SAFE_INTEGER) - Number(b.sortNo ?? Number.MAX_SAFE_INTEGER)
)
.map((item, index) => ({
...item,
planningId: item.planningId || formData.value.id || 0,
sortNo: item.sortNo ?? index + 1,
buildingType: item.buildingType ?? '',
remark: item.remark ?? ''
}))
const addGuideDetailRow = () => {
guideDetails.value.push(createGuideDetailRow(guideDetails.value.length + 1))
}
const removeGuideDetailRow = (index: number) => {
guideDetails.value.splice(index, 1)
resetGuideDetailSortNo()
}
const resetGuideDetailSortNo = () => {
guideDetails.value = guideDetails.value.map((item, index) => ({
...item,
sortNo: index + 1
}))
}
2026-05-09 11:11:56 +08:00
const hasGuideDetailRemark = (remark?: string) => Boolean(remark && remark.trim())
const formatGuideDetailRemarkPreview = (remark?: string) => {
if (!hasGuideDetailRemark(remark)) {
return '暂无备注'
}
return remark!.trim()
}
const openGuideDetailRemarkDialog = (row: GuideDetailRow) => {
activeRemarkRow.value = row
remarkDraft.value = row.remark || ''
remarkDialogVisible.value = true
}
const saveGuideDetailRemark = () => {
if (activeRemarkRow.value) {
activeRemarkRow.value.remark = remarkDraft.value
}
remarkDialogVisible.value = false
}
2026-04-29 15:44:00 +08:00
const getGuideDetailTotalAdjustmentFactor = (row: GuideDetailRow) => {
if (
row.drawingSetFactor === undefined ||
row.scaleFactor === undefined ||
row.modificationFactor === undefined ||
row.complexityFactor === undefined
) {
return undefined
}
return Number(
(
Number(row.drawingSetFactor) *
Number(row.scaleFactor) *
Number(row.modificationFactor) *
Number(row.complexityFactor)
).toFixed(4)
)
}
const getGuideDetailAssessmentArea = (row: GuideDetailRow) => {
const totalAdjustmentFactor = getGuideDetailTotalAdjustmentFactor(row)
if (row.designArea === undefined || totalAdjustmentFactor === undefined) {
return undefined
}
return Number((Number(row.designArea) * totalAdjustmentFactor).toFixed(2))
}
const getGuideDetailAssessmentOutputValue = (row: GuideDetailRow) => {
const assessmentArea = getGuideDetailAssessmentArea(row)
if (
row.internalGuidanceUnitPrice === undefined ||
row.designRatio === undefined ||
assessmentArea === undefined
) {
return undefined
}
return Number(
(
Number(row.internalGuidanceUnitPrice) *
assessmentArea *
Number(row.designRatio) *
(1 - Number(formData.value.reviewOutsourceRatio || 0))
).toFixed(2)
)
}
const guideDetailSummary = computed(() =>
guideDetails.value.reduce(
(summary, row) => ({
designArea: Number((summary.designArea + Number(row.designArea || 0)).toFixed(2)),
assessmentArea: Number(
(summary.assessmentArea + Number(getGuideDetailAssessmentArea(row) || 0)).toFixed(2)
),
assessmentOutputValue: Number(
(
summary.assessmentOutputValue + Number(getGuideDetailAssessmentOutputValue(row) || 0)
).toFixed(2)
)
}),
{
designArea: 0,
assessmentArea: 0,
assessmentOutputValue: 0
}
)
)
const contractUnitPricePreview = computed(() => {
const planningAmount = Number(formData.value.planningAmount || 0)
const planningArea = showGuideDetailSection.value
? Number(guideDetailSummary.value.designArea || 0)
: Number(formData.value.planningArea || 0)
if (!planningArea) {
return formatAmountText(0)
}
return formatAmountText(planningAmount / planningArea)
})
const formatFactorText = (value?: number, digits = 4) => {
if (value === undefined || value === null) {
return '-'
}
return Number(value).toFixed(digits)
}
2026-05-08 17:38:50 +08:00
const formatQuantityText = (value?: number, digits = 4) => {
if (value === undefined || value === null) {
return '-'
}
return Number(value).toFixed(digits)
}
2026-04-29 15:44:00 +08:00
const setGuideDetailPercentValue = (
row: GuideDetailRow,
field: 'complexityFactor' | 'designRatio',
value?: number | string | null
) => {
row[field] = fromPercentValue(value, 4)
}
const hasValue = (value: unknown) => value !== undefined && value !== null && value !== ''
2026-04-17 18:17:42 +08:00
const formRules = reactive<FormRules>({
2026-04-25 18:10:45 +08:00
calculationMethod: [{ required: true, message: '产值计算方式不能为空', trigger: 'change' }],
2026-04-17 18:17:42 +08:00
planningStartYear: [{ required: true, message: '开始年度不能为空', trigger: 'change' }],
2026-04-29 15:44:00 +08:00
planningArea: [
2026-04-17 18:17:42 +08:00
{
validator: (_rule, value, callback) => {
2026-04-29 15:44:00 +08:00
if (showGuideDetailSection.value) {
callback()
return
}
if (!hasValue(value)) {
callback(new Error('工程总面积不能为空'))
2026-04-17 18:17:42 +08:00
return
}
callback()
},
trigger: 'blur'
}
],
2026-04-29 15:44:00 +08:00
designStage: [{ required: true, message: '设计阶段不能为空', trigger: 'change' }],
currentDesignStageRatio: [
{ required: true, message: '本次设计阶段比例不能为空', trigger: 'blur' }
],
totalDistributionAmount: [{ required: true, message: '总分配不能为空', trigger: 'blur' }],
calculationRatio: [
2026-04-17 18:17:42 +08:00
{
validator: (_rule, value, callback) => {
2026-04-29 15:44:00 +08:00
if (showCalculationRatioField.value && !hasValue(value)) {
callback(new Error(`${calculationRatioLabel.value}不能为空`))
2026-04-17 18:17:42 +08:00
return
}
callback()
},
trigger: 'blur'
}
],
virtualCalculationMethod: [
{
validator: (_rule, value, callback) => {
if (showVirtualOutputSection.value && !value) {
callback(new Error('请选择虚拟产值计算方式'))
return
}
callback()
},
trigger: 'change'
}
2026-04-25 18:10:45 +08:00
],
workingDayCount: [
{
validator: (_rule, value, callback) => {
2026-04-29 15:44:00 +08:00
if (showWorkingDayFields.value && !hasValue(value)) {
2026-04-25 18:10:45 +08:00
callback(new Error('工日不能为空'))
return
}
callback()
},
trigger: 'blur'
}
],
workingDayUnitPrice: [
{
validator: (_rule, value, callback) => {
2026-04-29 15:44:00 +08:00
if (showWorkingDayFields.value && !hasValue(value)) {
2026-04-25 18:10:45 +08:00
callback(new Error('工日单价不能为空'))
return
}
callback()
},
trigger: 'blur'
}
],
guidanceUnitPrice: [
{
validator: (_rule, value, callback) => {
2026-04-29 15:44:00 +08:00
if (showGuidanceUnitPriceField.value && !hasValue(value)) {
2026-04-25 18:10:45 +08:00
callback(new Error('指导单价不能为空'))
return
}
callback()
},
trigger: 'blur'
}
],
guidanceTotalPrice: [
{
validator: (_rule, value, callback) => {
2026-04-29 15:44:00 +08:00
if (showGuidanceTotalPriceField.value && !hasValue(value)) {
2026-04-25 18:10:45 +08:00
callback(new Error('指导总价不能为空'))
return
}
callback()
},
trigger: 'blur'
}
2026-04-17 18:17:42 +08:00
]
})
const applyReviewOutsourceDefault = () => {
formData.value.reviewOutsourceRatio = fromPercentValue(
getReviewOutsourceDefaultPercent(
formData.value.ownershipType,
formData.value.reviewOutsourceFlag
)
)
}
const applyCalculationRatioDefault = () => {
const defaultPercent = getCalculationRatioDefaultPercent(formData.value.ownershipType)
if (defaultPercent === undefined) {
return
}
if (formData.value.calculationRatio === undefined || formData.value.calculationRatio === null) {
formData.value.calculationRatio = fromPercentValue(defaultPercent)
}
}
const handleReviewOutsourceFlagChange = () => {
applyReviewOutsourceDefault()
}
2026-04-25 18:10:45 +08:00
watch(
() => formData.value.virtualCalculationMethod,
(value) => {
if (
isWorkingDayMethod(value) &&
(formData.value.workingDayUnitPrice === undefined || formData.value.workingDayUnitPrice === null)
) {
formData.value.workingDayUnitPrice = DEFAULT_WORKING_DAY_UNIT_PRICE
}
}
)
2026-04-29 15:44:00 +08:00
watch(showGuideDetailSection, (value) => {
if (value && !guideDetails.value.length) {
addGuideDetailRow()
}
})
2026-04-17 18:17:42 +08:00
const buildSavePayload = (): PlanningApi.ProjectPlanningSaveVO => ({
id: formData.value.id,
projectId: formData.value.projectId,
ownershipType: formData.value.ownershipType,
calculationMethod: formData.value.calculationMethod,
planningContent: formData.value.planningContent,
2026-05-08 17:38:50 +08:00
sortNo: formData.value.sortNo,
contractValueQuantity: formData.value.contractValueQuantity,
contractValueUnitPrice: formData.value.contractValueUnitPrice,
2026-04-17 18:17:42 +08:00
managementFeeRate: formData.value.managementFeeRate,
2026-05-08 17:38:50 +08:00
vatRate: formData.value.vatRate,
2026-04-17 18:17:42 +08:00
implementationTeam: formData.value.implementationTeam,
planningStartYear: formData.value.planningStartYear,
2026-04-29 15:44:00 +08:00
planningArea: showGuideDetailSection.value
? guideDetailSummary.value.designArea
: formData.value.planningArea,
2026-04-17 18:17:42 +08:00
designStage: formData.value.designStage,
currentDesignStageRatio: formData.value.currentDesignStageRatio,
reviewOutsourceFlag: formData.value.reviewOutsourceFlag,
reviewOutsourceRatio: formData.value.reviewOutsourceRatio,
totalDistributionAmount: formData.value.totalDistributionAmount,
progressRemark: formData.value.progressRemark,
2026-04-29 15:44:00 +08:00
buildingOrUnitCount: showGuideDetailSection.value ? undefined : formData.value.buildingOrUnitCount,
drawingSetFactor: showGuideDetailSection.value ? undefined : formData.value.drawingSetFactor,
scaleFactor: showGuideDetailSection.value ? undefined : formData.value.scaleFactor,
modificationFactor: showGuideDetailSection.value ? undefined : formData.value.modificationFactor,
complexityFactor: showGuideDetailSection.value ? undefined : formData.value.complexityFactor,
internalGuidanceUnitPrice: showGuideDetailSection.value
? undefined
: formData.value.internalGuidanceUnitPrice,
2026-04-17 18:17:42 +08:00
virtualCalculationMethod: formData.value.virtualCalculationMethod,
workingDayCount: formData.value.workingDayCount,
workingDayUnitPrice: formData.value.workingDayUnitPrice,
guidanceUnitPrice: formData.value.guidanceUnitPrice,
guidanceTotalPrice: formData.value.guidanceTotalPrice,
calculationRatio: formData.value.calculationRatio
})
2026-04-29 15:44:00 +08:00
const buildGuideDetailPayload = (): PlanningGuideDetailApi.ProjectPlanningGuideDetailBatchSaveVO => ({
planningId: formData.value.id!,
details: guideDetails.value
.map((item, index) => ({
id: item.id,
designPart: item.designPart,
buildingType: item.buildingType,
designArea: item.designArea,
internalGuidanceUnitPrice: item.internalGuidanceUnitPrice,
buildingOrUnitCount: item.buildingOrUnitCount,
drawingSetFactor: item.drawingSetFactor,
scaleFactor: item.scaleFactor,
modificationFactor: item.modificationFactor,
complexityFactor: item.complexityFactor,
designRatio: item.designRatio,
sortNo: item.sortNo ?? index + 1,
remark: item.remark
}))
.sort((a, b) => Number(a.sortNo || 0) - Number(b.sortNo || 0))
})
const validateGuideDetails = () => {
if (!guideDetails.value.length) {
message.warning('请至少维护一条指导价法明细')
return false
}
for (let index = 0; index < guideDetails.value.length; index++) {
const row = guideDetails.value[index]
const rowText = `${index + 1}`
if (!row.designPart) {
message.warning(`${rowText}设计部位不能为空`)
return false
}
if (!row.buildingType) {
message.warning(`${rowText}建筑类型不能为空`)
return false
}
if (!hasValue(row.designArea)) {
message.warning(`${rowText}设计面积不能为空`)
return false
}
if (!hasValue(row.internalGuidanceUnitPrice)) {
message.warning(`${rowText}内部指导单价不能为空`)
return false
}
if (!hasValue(row.drawingSetFactor)) {
message.warning(`${rowText}套图系数不能为空`)
return false
}
if (!hasValue(row.scaleFactor)) {
message.warning(`${rowText}规模系数不能为空`)
return false
}
if (!hasValue(row.modificationFactor)) {
message.warning(`${rowText}修改系数不能为空`)
return false
}
if (!hasValue(row.complexityFactor)) {
message.warning(`${rowText}复杂系数不能为空`)
return false
}
if (!hasValue(row.designRatio)) {
message.warning(`${rowText}设计占比不能为空`)
return false
}
}
return true
}
2026-04-17 18:17:42 +08:00
const open = async (id: number) => {
dialogVisible.value = true
formLoading.value = true
try {
const data = await PlanningApi.getProjectPlanning(id)
formData.value = normalizeFormData(data)
2026-04-29 15:44:00 +08:00
guideDetails.value = showGuideDetailSection.value
? normalizeGuideDetailList(
await PlanningGuideDetailApi.getProjectPlanningGuideDetailListByPlanningId(id)
)
: []
2026-04-17 18:17:42 +08:00
applyCalculationRatioDefault()
if (formData.value.reviewOutsourceRatio === undefined || formData.value.reviewOutsourceRatio === null) {
applyReviewOutsourceDefault()
}
2026-04-29 15:44:00 +08:00
if (showGuideDetailSection.value && !guideDetails.value.length) {
addGuideDetailRow()
}
2026-04-17 18:17:42 +08:00
} finally {
formLoading.value = false
}
}
2026-04-29 15:44:00 +08:00
2026-04-17 18:17:42 +08:00
defineExpose({ open })
const emit = defineEmits(['success'])
const submitForm = async () => {
if (!formRef.value) {
return
}
const valid = await formRef.value.validate()
if (!valid) {
return
}
2026-04-29 15:44:00 +08:00
if (showGuideDetailSection.value && !validateGuideDetails()) {
return
}
2026-04-17 18:17:42 +08:00
formLoading.value = true
try {
await PlanningApi.updateProjectPlanning(buildSavePayload())
2026-04-29 15:44:00 +08:00
if (showGuideDetailSection.value && formData.value.id) {
await PlanningGuideDetailApi.batchSaveProjectPlanningGuideDetail(buildGuideDetailPayload())
}
message.success('保存成功')
2026-04-17 18:17:42 +08:00
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
</script>
2026-04-29 15:44:00 +08:00
<style scoped>
/* 优化无控制按钮的 Input Number 显示样式 */
:deep(.optimized-table .el-input-number .el-input__inner) {
text-align: left;
}
/* 给只读计算结果列添加淡淡的背景色用于视觉区分 */
:deep(.bg-gray-50) {
background-color: #f9fafc !important;
}
:deep(.text-primary) {
color: var(--el-color-primary);
}
2026-05-09 11:11:56 +08:00
.remark-cell {
display: flex;
min-height: 32px;
align-items: center;
justify-content: center;
gap: 8px;
}
.remark-preview {
min-width: 0;
overflow: hidden;
flex: 1;
color: var(--el-text-color-regular);
font-size: 13px;
line-height: 20px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
.remark-preview.is-empty {
color: var(--el-text-color-placeholder);
}
.remark-edit-button {
flex: none;
}
:global(.guide-remark-tooltip) {
max-width: 640px;
}
:global(.guide-remark-tooltip .remark-tooltip-content) {
max-height: 260px;
overflow-y: auto;
line-height: 22px;
white-space: pre-line;
word-break: break-all;
}
2026-05-08 17:38:50 +08:00
</style>