Files
tjt_czjs_ui/src/views/tjt/output/PlanningOutputForm.vue
2026-05-09 11:11:56 +08:00

1299 lines
43 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<Dialog v-model="dialogVisible" title="编辑测算参数" width="92%">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="150px"
>
<el-divider content-position="left">规划信息</el-divider>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="归属类型">
<el-input :model-value="getOwnershipTypeLabel(formData.ownershipType)" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="产值计算方式" prop="calculationMethod">
<el-select
v-model="formData.calculationMethod"
class="!w-1/1"
placeholder="请选择产值计算方式"
>
<el-option
v-for="item in CALCULATION_METHOD_OPTIONS"
:key="String(item.value)"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目任务包">
<el-input :model-value="formData.planningContent" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<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="分项合同产值(元)">
<el-input :model-value="formatAmountText(formData.planningAmount)" disabled />
</el-form-item>
</el-col>
</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>
<el-col :span="8">
<el-form-item label="管理费率(%)">
<el-input :model-value="formatPercentText(formData.managementFeeRate)" disabled />
</el-form-item>
</el-col>
</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>
<el-col :span="8">
<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="意向实施团队">
<el-input :model-value="formData.implementationTeam || '-'" disabled />
</el-form-item>
</el-col>
</el-row>
<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">
<el-form-item label="工程总面积(m²)" prop="planningArea">
<el-input
v-if="showGuideDetailSection"
:model-value="formatAmountText(guideDetailSummary.designArea)"
disabled
/>
<el-input-number
v-else
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">
<el-form-item label="合同单价(元/m²)">
<el-input :model-value="contractUnitPricePreview" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="设计阶段" prop="designStage">
<el-select v-model="formData.designStage" class="!w-1/1" placeholder="请选择设计阶段">
<el-option
v-for="item in DESIGN_STAGE_OPTIONS"
:key="String(item.value)"
: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>
</el-row>
<el-row :gutter="16">
<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>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<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>
<template v-if="showParentMajorFactorFields">
<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"
:precision="2"
: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"
:precision="2"
: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"
:precision="2"
: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>
<!-- 优化后的指导价法明细块 -->
<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 }">
<el-tooltip
:content="row.buildingType"
:disabled="!hasValue(row.buildingType)"
placement="top"
>
<el-input v-model="row.buildingType" maxlength="100" placeholder="建筑类型" />
</el-tooltip>
</template>
</el-table-column>
<!-- 2. 取消 controls 的核心数值列 -->
<el-table-column align="center" label="指导单价(元)" min-width="110">
<template #default="{ row }">
<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>
</template>
</el-table-column>
<el-table-column align="center" label="设计面积(m²)" min-width="110">
<template #default="{ row }">
<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>
</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 }">
<el-input-number v-model="row.drawingSetFactor" :min="0" :precision="2" :controls="false" class="!w-1/1" />
</template>
</el-table-column>
<el-table-column align="center" label="规模" min-width="85">
<template #default="{ row }">
<el-input-number v-model="row.scaleFactor" :min="0" :precision="2" :controls="false" class="!w-1/1" />
</template>
</el-table-column>
<el-table-column align="center" label="修改" min-width="85">
<template #default="{ row }">
<el-input-number v-model="row.modificationFactor" :min="0" :precision="2" :controls="false" class="!w-1/1" />
</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>
<el-table-column align="left" label="备注" min-width="260">
<template #default="{ row }">
<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>
</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
/>
</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
v-for="item in VIRTUAL_CALCULATION_METHOD_OPTIONS"
:key="String(item.value)"
: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>
<el-row v-if="showGuidanceUnitPriceField" :gutter="16">
<el-col :span="8">
<el-form-item label="指导单价(元/m²)" prop="guidanceUnitPrice">
<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>
</el-row>
<el-row v-if="showGuidanceTotalPriceField" :gutter="16">
<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>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">保存</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</template>
</Dialog>
<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>
</template>
<script lang="ts" setup>
// [原有的 Script 逻辑不变,直接粘贴原本的逻辑即可]
import type { FormRules } from 'element-plus'
import * as PlanningApi from '@/api/tjt/planning'
import * as PlanningGuideDetailApi from '@/api/tjt/planningGuideDetail'
import {
CALCULATION_METHOD_OPTIONS,
DEFAULT_WORKING_DAY_UNIT_PRICE,
DESIGN_PART_OPTIONS,
DESIGN_STAGE_OPTIONS,
VIRTUAL_CALCULATION_METHOD_OPTIONS,
formatAmountText,
formatPercentText,
fromPercentValue,
getCalculationRatioDefaultPercent,
getCalculationRatioLabel,
getOwnershipTypeLabel,
getReviewOutsourceDefaultPercent,
isComprehensiveOwnership,
isContractPriceMethod,
isGuidancePriceMethod,
isMajorOwnership,
isSubcontractOwnership,
isVirtualGuidanceMethod,
isVirtualGuidanceTotalPriceMethod,
isVirtualOutputMethod,
isWorkingDayMethod,
normalizeCalculationMethod,
normalizeDesignStage,
normalizeOwnershipType,
normalizeVirtualCalculationMethod,
toPercentValue
} from '@/views/tjt/shared/planning'
defineOptions({ name: 'TjtPlanningOutputForm' })
type GuideDetailRow = PlanningGuideDetailApi.ProjectPlanningGuideDetailVO
const message = useMessage()
const dialogVisible = ref(false)
const formLoading = ref(false)
const formRef = ref()
const createFormData = (): PlanningApi.ProjectPlanningVO => ({
id: undefined,
projectId: 0,
ownershipType: '',
calculationMethod: '',
planningContent: '',
planningAmount: undefined,
contractValueQuantity: undefined,
contractValueUnitPrice: undefined,
managementFeeRate: undefined,
managementFee: undefined,
vatRate: undefined,
vatAmount: undefined,
projectBudgetOutputValue: undefined,
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
})
const formData = ref<PlanningApi.ProjectPlanningVO>(createFormData())
const guideDetails = ref<GuideDetailRow[]>([])
const remarkDialogVisible = ref(false)
const activeRemarkRow = ref<GuideDetailRow>()
const remarkDraft = ref('')
const planningStartYearValue = computed({
get: () => (formData.value.planningStartYear ? String(formData.value.planningStartYear) : undefined),
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)
)
const showGuideDetailSection = computed(
() =>
isMajorOwnership(formData.value.ownershipType) &&
isGuidancePriceMethod(formData.value.calculationMethod)
)
const showParentMajorFactorFields = computed(
() =>
isMajorOwnership(formData.value.ownershipType) &&
isContractPriceMethod(formData.value.calculationMethod)
)
const showVirtualOutputSection = computed(
() =>
isMajorOwnership(formData.value.ownershipType) &&
isVirtualOutputMethod(formData.value.calculationMethod)
)
const showWorkingDayFields = computed(() => isWorkingDayMethod(formData.value.virtualCalculationMethod))
const showGuidanceUnitPriceField = computed(
() => isVirtualGuidanceMethod(formData.value.virtualCalculationMethod)
)
const showGuidanceTotalPriceField = computed(
() => isVirtualGuidanceTotalPriceMethod(formData.value.virtualCalculationMethod)
)
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 || '',
contractValueQuantity: data.contractValueQuantity ?? 1,
contractValueUnitPrice: data.contractValueUnitPrice ?? data.planningAmount,
vatRate: data.vatRate ?? 0.06,
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
}))
}
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
}
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)
}
const formatQuantityText = (value?: number, digits = 4) => {
if (value === undefined || value === null) {
return '-'
}
return Number(value).toFixed(digits)
}
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 !== ''
const formRules = reactive<FormRules>({
calculationMethod: [{ required: true, message: '产值计算方式不能为空', trigger: 'change' }],
planningStartYear: [{ required: true, message: '开始年度不能为空', trigger: 'change' }],
planningArea: [
{
validator: (_rule, value, callback) => {
if (showGuideDetailSection.value) {
callback()
return
}
if (!hasValue(value)) {
callback(new Error('工程总面积不能为空'))
return
}
callback()
},
trigger: 'blur'
}
],
designStage: [{ required: true, message: '设计阶段不能为空', trigger: 'change' }],
currentDesignStageRatio: [
{ required: true, message: '本次设计阶段比例不能为空', trigger: 'blur' }
],
totalDistributionAmount: [{ required: true, message: '总分配不能为空', trigger: 'blur' }],
calculationRatio: [
{
validator: (_rule, value, callback) => {
if (showCalculationRatioField.value && !hasValue(value)) {
callback(new Error(`${calculationRatioLabel.value}不能为空`))
return
}
callback()
},
trigger: 'blur'
}
],
virtualCalculationMethod: [
{
validator: (_rule, value, callback) => {
if (showVirtualOutputSection.value && !value) {
callback(new Error('请选择虚拟产值计算方式'))
return
}
callback()
},
trigger: 'change'
}
],
workingDayCount: [
{
validator: (_rule, value, callback) => {
if (showWorkingDayFields.value && !hasValue(value)) {
callback(new Error('工日不能为空'))
return
}
callback()
},
trigger: 'blur'
}
],
workingDayUnitPrice: [
{
validator: (_rule, value, callback) => {
if (showWorkingDayFields.value && !hasValue(value)) {
callback(new Error('工日单价不能为空'))
return
}
callback()
},
trigger: 'blur'
}
],
guidanceUnitPrice: [
{
validator: (_rule, value, callback) => {
if (showGuidanceUnitPriceField.value && !hasValue(value)) {
callback(new Error('指导单价不能为空'))
return
}
callback()
},
trigger: 'blur'
}
],
guidanceTotalPrice: [
{
validator: (_rule, value, callback) => {
if (showGuidanceTotalPriceField.value && !hasValue(value)) {
callback(new Error('指导总价不能为空'))
return
}
callback()
},
trigger: 'blur'
}
]
})
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()
}
watch(
() => formData.value.virtualCalculationMethod,
(value) => {
if (
isWorkingDayMethod(value) &&
(formData.value.workingDayUnitPrice === undefined || formData.value.workingDayUnitPrice === null)
) {
formData.value.workingDayUnitPrice = DEFAULT_WORKING_DAY_UNIT_PRICE
}
}
)
watch(showGuideDetailSection, (value) => {
if (value && !guideDetails.value.length) {
addGuideDetailRow()
}
})
const buildSavePayload = (): PlanningApi.ProjectPlanningSaveVO => ({
id: formData.value.id,
projectId: formData.value.projectId,
ownershipType: formData.value.ownershipType,
calculationMethod: formData.value.calculationMethod,
planningContent: formData.value.planningContent,
sortNo: formData.value.sortNo,
contractValueQuantity: formData.value.contractValueQuantity,
contractValueUnitPrice: formData.value.contractValueUnitPrice,
managementFeeRate: formData.value.managementFeeRate,
vatRate: formData.value.vatRate,
implementationTeam: formData.value.implementationTeam,
planningStartYear: formData.value.planningStartYear,
planningArea: showGuideDetailSection.value
? guideDetailSummary.value.designArea
: formData.value.planningArea,
designStage: formData.value.designStage,
currentDesignStageRatio: formData.value.currentDesignStageRatio,
reviewOutsourceFlag: formData.value.reviewOutsourceFlag,
reviewOutsourceRatio: formData.value.reviewOutsourceRatio,
totalDistributionAmount: formData.value.totalDistributionAmount,
progressRemark: formData.value.progressRemark,
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,
virtualCalculationMethod: formData.value.virtualCalculationMethod,
workingDayCount: formData.value.workingDayCount,
workingDayUnitPrice: formData.value.workingDayUnitPrice,
guidanceUnitPrice: formData.value.guidanceUnitPrice,
guidanceTotalPrice: formData.value.guidanceTotalPrice,
calculationRatio: formData.value.calculationRatio
})
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
}
const open = async (id: number) => {
dialogVisible.value = true
formLoading.value = true
try {
const data = await PlanningApi.getProjectPlanning(id)
formData.value = normalizeFormData(data)
guideDetails.value = showGuideDetailSection.value
? normalizeGuideDetailList(
await PlanningGuideDetailApi.getProjectPlanningGuideDetailListByPlanningId(id)
)
: []
applyCalculationRatioDefault()
if (formData.value.reviewOutsourceRatio === undefined || formData.value.reviewOutsourceRatio === null) {
applyReviewOutsourceDefault()
}
if (showGuideDetailSection.value && !guideDetails.value.length) {
addGuideDetailRow()
}
} finally {
formLoading.value = false
}
}
defineExpose({ open })
const emit = defineEmits(['success'])
const submitForm = async () => {
if (!formRef.value) {
return
}
const valid = await formRef.value.validate()
if (!valid) {
return
}
if (showGuideDetailSection.value && !validateGuideDetails()) {
return
}
formLoading.value = true
try {
await PlanningApi.updateProjectPlanning(buildSavePayload())
if (showGuideDetailSection.value && formData.value.id) {
await PlanningGuideDetailApi.batchSaveProjectPlanningGuideDetail(buildGuideDetailPayload())
}
message.success('保存成功')
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
</script>
<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);
}
.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;
}
</style>