829 lines
28 KiB
Vue
829 lines
28 KiB
Vue
|
|
<template>
|
|||
|
|
<ContentWrap>
|
|||
|
|
<el-form
|
|||
|
|
ref="queryFormRef"
|
|||
|
|
:inline="true"
|
|||
|
|
:model="queryParams"
|
|||
|
|
class="-mb-15px"
|
|||
|
|
label-width="88px"
|
|||
|
|
>
|
|||
|
|
<el-form-item label="工程名称" prop="projectName">
|
|||
|
|
<el-input
|
|||
|
|
v-model="queryParams.projectName"
|
|||
|
|
class="!w-240px"
|
|||
|
|
clearable
|
|||
|
|
placeholder="请输入工程名称"
|
|||
|
|
@keyup.enter="handleQuery"
|
|||
|
|
/>
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="开始年度" prop="projectStartYear">
|
|||
|
|
<el-date-picker
|
|||
|
|
v-model="queryProjectStartYearValue"
|
|||
|
|
class="!w-180px"
|
|||
|
|
clearable
|
|||
|
|
placeholder="请选择年度"
|
|||
|
|
type="year"
|
|||
|
|
value-format="YYYY"
|
|||
|
|
/>
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item>
|
|||
|
|
<el-button @click="handleQuery">
|
|||
|
|
<Icon class="mr-5px" icon="ep:search" />
|
|||
|
|
搜索
|
|||
|
|
</el-button>
|
|||
|
|
<el-button @click="resetQuery">
|
|||
|
|
<Icon class="mr-5px" icon="ep:refresh" />
|
|||
|
|
重置
|
|||
|
|
</el-button>
|
|||
|
|
</el-form-item>
|
|||
|
|
</el-form>
|
|||
|
|
</ContentWrap>
|
|||
|
|
|
|||
|
|
<ContentWrap>
|
|||
|
|
<el-table
|
|||
|
|
ref="projectTableRef"
|
|||
|
|
v-loading="loading"
|
|||
|
|
:data="projectList"
|
|||
|
|
highlight-current-row
|
|||
|
|
@current-change="handleCurrentProjectChange"
|
|||
|
|
>
|
|||
|
|
<el-table-column align="center" label="项目 ID" prop="id" width="88" />
|
|||
|
|
<el-table-column align="center" label="工程名称" min-width="220" prop="projectName" />
|
|||
|
|
<el-table-column align="center" label="项目经理" min-width="120" prop="projectManagerName" />
|
|||
|
|
<el-table-column
|
|||
|
|
align="center"
|
|||
|
|
label="工程负责人"
|
|||
|
|
min-width="120"
|
|||
|
|
prop="engineeringPrincipalName"
|
|||
|
|
/>
|
|||
|
|
<el-table-column align="center" label="项目开始年度" prop="projectStartYear" width="120" />
|
|||
|
|
</el-table>
|
|||
|
|
<Pagination
|
|||
|
|
v-model:limit="queryParams.pageSize"
|
|||
|
|
v-model:page="queryParams.pageNo"
|
|||
|
|
:total="total"
|
|||
|
|
@pagination="getProjectList"
|
|||
|
|
/>
|
|||
|
|
</ContentWrap>
|
|||
|
|
|
|||
|
|
<el-row :gutter="16">
|
|||
|
|
<el-col :span="8">
|
|||
|
|
<ContentWrap>
|
|||
|
|
<div class="mb-12px flex items-center justify-between">
|
|||
|
|
<div class="text-14px font-600">
|
|||
|
|
{{ currentProject?.projectName || '专业所规划列表' }}
|
|||
|
|
</div>
|
|||
|
|
<el-button v-if="currentProject" size="small" @click="getPlanningList">刷新</el-button>
|
|||
|
|
</div>
|
|||
|
|
<el-table
|
|||
|
|
ref="planningTableRef"
|
|||
|
|
v-loading="planningLoading"
|
|||
|
|
:data="planningList"
|
|||
|
|
highlight-current-row
|
|||
|
|
@current-change="handleCurrentPlanningChange"
|
|||
|
|
>
|
|||
|
|
<el-table-column align="center" label="规划内容" min-width="180" prop="planningContent" />
|
|||
|
|
<el-table-column align="center" label="开始年度" prop="planningStartYear" width="100" />
|
|||
|
|
<el-table-column align="center" label="考核产值(元)" width="120">
|
|||
|
|
<template #default="scope">
|
|||
|
|
{{ formatAmountText(scope.row.assessmentOutputValue) }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
</ContentWrap>
|
|||
|
|
</el-col>
|
|||
|
|
|
|||
|
|
<el-col :span="16">
|
|||
|
|
<ContentWrap v-if="currentPlanning && formData">
|
|||
|
|
<div class="mb-16px flex items-center justify-between gap-16px">
|
|||
|
|
<div>
|
|||
|
|
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
|
|||
|
|
<div class="mt-4px text-13px text-[var(--el-text-color-secondary)]">
|
|||
|
|
年度:{{ formData.year || '-' }},考核产值:{{ formatAmountText(formData.assessmentOutputValue) }} 元
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center gap-12px">
|
|||
|
|
<el-button
|
|||
|
|
v-hasPermi="['tjt:output-split:update']"
|
|||
|
|
plain
|
|||
|
|
type="primary"
|
|||
|
|
@click="openEditDialog"
|
|||
|
|
>
|
|||
|
|
<Icon class="mr-5px" icon="ep:edit" />
|
|||
|
|
编辑比例
|
|||
|
|
</el-button>
|
|||
|
|
<el-button @click="refreshCurrentPlanning">
|
|||
|
|
<Icon class="mr-5px" icon="ep:refresh" />
|
|||
|
|
刷新结果
|
|||
|
|
</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-descriptions :column="2" border title="基础信息">
|
|||
|
|
<el-descriptions-item label="项目名称">{{ formData.projectName || '-' }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="规划内容">{{ formData.planningContent || '-' }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="项目经理">{{ formData.projectManagerName || '-' }}</el-descriptions-item>
|
|||
|
|
<el-descriptions-item label="项目负责人">{{ formData.engineeringLeaderName || '-' }}</el-descriptions-item>
|
|||
|
|
</el-descriptions>
|
|||
|
|
|
|||
|
|
<el-divider content-position="left">项目层结果</el-divider>
|
|||
|
|
<el-table :data="projectResultRows" border>
|
|||
|
|
<el-table-column align="center" label="类别" min-width="140" prop="label" />
|
|||
|
|
<el-table-column align="center" label="比例" min-width="120">
|
|||
|
|
<template #default="scope">
|
|||
|
|
{{ scope.row.percentText }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column align="center" label="金额(元)" min-width="140">
|
|||
|
|
<template #default="scope">
|
|||
|
|
{{ formatAmountText(scope.row.amount) }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<el-divider content-position="left">专业层结果</el-divider>
|
|||
|
|
<el-table :data="specialtyResultRows" border>
|
|||
|
|
<el-table-column align="center" label="专业" min-width="140" prop="label" />
|
|||
|
|
<el-table-column align="center" label="比例" min-width="120">
|
|||
|
|
<template #default="scope">
|
|||
|
|
{{ scope.row.percentText }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column align="center" label="金额(元)" min-width="140">
|
|||
|
|
<template #default="scope">
|
|||
|
|
{{ formatAmountText(scope.row.amount) }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<el-divider content-position="left">年度分配信息</el-divider>
|
|||
|
|
<div class="mb-12px flex flex-wrap items-start justify-between gap-12px">
|
|||
|
|
<el-radio-group v-model="selectedAnnualCategory" size="small">
|
|||
|
|
<el-radio-button
|
|||
|
|
v-for="item in annualCategoryOptions"
|
|||
|
|
:key="item.value"
|
|||
|
|
:label="item.value"
|
|||
|
|
>
|
|||
|
|
{{ item.label }}
|
|||
|
|
</el-radio-button>
|
|||
|
|
</el-radio-group>
|
|||
|
|
</div>
|
|||
|
|
<el-row :gutter="16" class="mb-16px">
|
|||
|
|
<el-col v-for="item in annualSummaryCards" :key="item.label" :span="8">
|
|||
|
|
<div class="rounded-8px bg-[var(--el-fill-color-light)] px-16px py-12px">
|
|||
|
|
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
|||
|
|
{{ item.label }}
|
|||
|
|
</div>
|
|||
|
|
<div class="mt-6px text-18px font-600">
|
|||
|
|
{{ formatAmountText(item.amount) }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</el-col>
|
|||
|
|
</el-row>
|
|||
|
|
<el-table v-loading="quarterLoading" :data="annualDistributionRows" border>
|
|||
|
|
<el-table-column align="center" label="分配年度" min-width="120" prop="distributionYear" />
|
|||
|
|
<el-table-column
|
|||
|
|
v-for="quarter in QUARTER_OPTIONS"
|
|||
|
|
:key="String(quarter.value)"
|
|||
|
|
align="center"
|
|||
|
|
:label="quarter.label"
|
|||
|
|
min-width="150"
|
|||
|
|
>
|
|||
|
|
<template #default="scope">
|
|||
|
|
{{ formatAmountText(scope.row.quarterAmounts[Number(quarter.value)]) }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column align="center" label="年度合计(元)" min-width="160">
|
|||
|
|
<template #default="scope">
|
|||
|
|
{{ formatAmountText(scope.row.yearTotal) }}
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
</ContentWrap>
|
|||
|
|
|
|||
|
|
<ContentWrap v-else>
|
|||
|
|
<el-empty description="请选择专业所规划后查看页面4结果" />
|
|||
|
|
</ContentWrap>
|
|||
|
|
</el-col>
|
|||
|
|
</el-row>
|
|||
|
|
|
|||
|
|
<Dialog v-model="dialogVisible" title="编辑专业及项目总分配比例" width="900">
|
|||
|
|
<template v-if="editForm">
|
|||
|
|
|
|||
|
|
<el-divider content-position="left">项目层比例编辑</el-divider>
|
|||
|
|
<el-row :gutter="16">
|
|||
|
|
<el-col v-for="item in editProjectRatioItems" :key="item.key" :span="8">
|
|||
|
|
<div class="rounded-8px border border-solid border-[var(--el-border-color)] p-12px">
|
|||
|
|
<div class="mb-8px text-13px font-600">{{ item.label }}</div>
|
|||
|
|
<el-input-number
|
|||
|
|
:model-value="item.percent"
|
|||
|
|
:disabled="item.key === 'officeRatio'"
|
|||
|
|
:min="0"
|
|||
|
|
:precision="2"
|
|||
|
|
:step="0.1"
|
|||
|
|
class="!w-1/1"
|
|||
|
|
controls-position="right"
|
|||
|
|
@update:model-value="(value) => updateProjectRatio(item.key, value)"
|
|||
|
|
/>
|
|||
|
|
<div class="mt-8px text-12px text-[var(--el-text-color-secondary)]">
|
|||
|
|
金额:{{ formatAmountText(item.amount) }} 元
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</el-col>
|
|||
|
|
</el-row>
|
|||
|
|
<div class="mt-8px text-12px" :class="projectRatioTotalClass">
|
|||
|
|
项目层比例合计:{{ projectRatioTotalText }}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-divider content-position="left">专业层比例编辑</el-divider>
|
|||
|
|
<el-row :gutter="16">
|
|||
|
|
<el-col v-for="item in editSpecialtyRatioItems" :key="item.key" :span="8" class="mb-12px">
|
|||
|
|
<div class="rounded-8px border border-solid border-[var(--el-border-color)] p-12px">
|
|||
|
|
<div class="mb-8px text-13px font-600">{{ item.label }}</div>
|
|||
|
|
<el-input-number
|
|||
|
|
:model-value="item.percent"
|
|||
|
|
:disabled="item.key === 'archRatio'"
|
|||
|
|
:min="0"
|
|||
|
|
:precision="2"
|
|||
|
|
:step="0.1"
|
|||
|
|
class="!w-1/1"
|
|||
|
|
controls-position="right"
|
|||
|
|
@update:model-value="(value) => updateSpecialtyRatio(item.key, value)"
|
|||
|
|
/>
|
|||
|
|
<div class="mt-8px text-12px text-[var(--el-text-color-secondary)]">
|
|||
|
|
金额:{{ formatAmountText(item.amount) }} 元
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</el-col>
|
|||
|
|
</el-row>
|
|||
|
|
<div class="mt-8px text-12px" :class="specialtyRatioTotalClass">
|
|||
|
|
专业层比例合计:{{ specialtyRatioTotalText }}
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
<template #footer>
|
|||
|
|
<el-button :loading="saveLoading" type="primary" @click="handleSave">保存</el-button>
|
|||
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|||
|
|
</template>
|
|||
|
|
</Dialog>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script lang="ts" setup>
|
|||
|
|
import * as ProjectApi from '@/api/tjt/project'
|
|||
|
|
import * as PlanningApi from '@/api/tjt/planning'
|
|||
|
|
import * as PlanningQuarterApi from '@/api/tjt/planningQuarter'
|
|||
|
|
import * as OutputSplitApi from '@/api/tjt/outputSplit'
|
|||
|
|
import {
|
|||
|
|
formatAmountText,
|
|||
|
|
fromPercentValue,
|
|||
|
|
isMajorOwnership,
|
|||
|
|
OUTPUT_SPLIT_SPECIALTY_OPTIONS,
|
|||
|
|
QUARTER_OPTIONS,
|
|||
|
|
toPercentValue
|
|||
|
|
} from '@/views/tjt/shared/planning'
|
|||
|
|
|
|||
|
|
defineOptions({ name: 'TjtOutputSplit' })
|
|||
|
|
|
|||
|
|
type AnnualCategoryKey =
|
|||
|
|
| 'projectManager'
|
|||
|
|
| 'engineeringLeader'
|
|||
|
|
| 'arch'
|
|||
|
|
| 'decor'
|
|||
|
|
| 'struct'
|
|||
|
|
| 'water'
|
|||
|
|
| 'elec'
|
|||
|
|
| 'hvac'
|
|||
|
|
| 'digital'
|
|||
|
|
|
|||
|
|
interface QuarterYearRow {
|
|||
|
|
distributionYear: number
|
|||
|
|
quarters: PlanningQuarterApi.ProjectPlanningQuarterVO[]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const annualCategoryOptions: { label: string; value: AnnualCategoryKey }[] = [
|
|||
|
|
{ label: '项目经理', value: 'projectManager' },
|
|||
|
|
{ label: '项目负责人', value: 'engineeringLeader' },
|
|||
|
|
{ label: '建筑专业', value: 'arch' },
|
|||
|
|
{ label: '装修专业', value: 'decor' },
|
|||
|
|
{ label: '结构专业', value: 'struct' },
|
|||
|
|
{ label: '水专业', value: 'water' },
|
|||
|
|
{ label: '电气专业', value: 'elec' },
|
|||
|
|
{ label: '暖通专业', value: 'hvac' },
|
|||
|
|
{ label: '数字化设计专业', value: 'digital' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const message = useMessage()
|
|||
|
|
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const planningLoading = ref(false)
|
|||
|
|
const quarterLoading = ref(false)
|
|||
|
|
const saveLoading = ref(false)
|
|||
|
|
const total = ref(0)
|
|||
|
|
const projectList = ref<ProjectApi.ProjectVO[]>([])
|
|||
|
|
const planningList = ref<PlanningApi.ProjectPlanningVO[]>([])
|
|||
|
|
const currentProject = ref<ProjectApi.ProjectVO>()
|
|||
|
|
const currentPlanning = ref<PlanningApi.ProjectPlanningVO>()
|
|||
|
|
const formData = ref<OutputSplitApi.ProjectOutputSplitVO>()
|
|||
|
|
const editForm = ref<OutputSplitApi.ProjectOutputSplitVO>()
|
|||
|
|
const quarterRows = ref<QuarterYearRow[]>([])
|
|||
|
|
const selectedAnnualCategory = ref<AnnualCategoryKey>('projectManager')
|
|||
|
|
const dialogVisible = ref(false)
|
|||
|
|
const queryFormRef = ref()
|
|||
|
|
const projectTableRef = ref()
|
|||
|
|
const planningTableRef = ref()
|
|||
|
|
|
|||
|
|
const queryParams = reactive<ProjectApi.ProjectPageReqVO>({
|
|||
|
|
pageNo: 1,
|
|||
|
|
pageSize: 10,
|
|||
|
|
projectName: undefined,
|
|||
|
|
projectStartYear: undefined
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const queryProjectStartYearValue = computed({
|
|||
|
|
get: () => (queryParams.projectStartYear ? String(queryParams.projectStartYear) : undefined),
|
|||
|
|
set: (value?: string) => {
|
|||
|
|
queryParams.projectStartYear = value ? Number(value) : undefined
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const toNumeric = (value?: number | string | null) => {
|
|||
|
|
const numericValue = Number(value ?? 0)
|
|||
|
|
return Number.isNaN(numericValue) ? 0 : numericValue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const multiplyAmount = (...values: Array<number | string | null | undefined>) =>
|
|||
|
|
Number(values.reduce((result, value) => result * toNumeric(value), 1).toFixed(2))
|
|||
|
|
|
|||
|
|
const formatRatioText = (value?: number | string | null) => `${(toPercentValue(value) ?? 0).toFixed(2)}%`
|
|||
|
|
|
|||
|
|
const buildProjectRows = (model?: OutputSplitApi.ProjectOutputSplitVO) => {
|
|||
|
|
if (!model) {
|
|||
|
|
return []
|
|||
|
|
}
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
key: 'projectManagerRatio',
|
|||
|
|
label: '项目经理',
|
|||
|
|
percentText: formatRatioText(model.projectManagerRatio),
|
|||
|
|
percent: toPercentValue(model.projectManagerRatio),
|
|||
|
|
amount: model.projectManagerAmount
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'engineeringLeaderRatio',
|
|||
|
|
label: '项目负责人',
|
|||
|
|
percentText: formatRatioText(model.engineeringLeaderRatio),
|
|||
|
|
percent: toPercentValue(model.engineeringLeaderRatio),
|
|||
|
|
amount: model.engineeringLeaderAmount
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: 'officeRatio',
|
|||
|
|
label: '专业所',
|
|||
|
|
percentText: formatRatioText(model.officeRatio),
|
|||
|
|
percent: toPercentValue(model.officeRatio),
|
|||
|
|
amount: model.officeAmount
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const buildSpecialtyRows = (model?: OutputSplitApi.ProjectOutputSplitVO) => {
|
|||
|
|
if (!model) {
|
|||
|
|
return []
|
|||
|
|
}
|
|||
|
|
const amountMap: Record<string, number | undefined> = {
|
|||
|
|
arch: model.archAmount,
|
|||
|
|
decor: model.decorAmount,
|
|||
|
|
struct: model.structAmount,
|
|||
|
|
water: model.waterAmount,
|
|||
|
|
elec: model.elecAmount,
|
|||
|
|
hvac: model.hvacAmount,
|
|||
|
|
digital: model.digitalAmount
|
|||
|
|
}
|
|||
|
|
return OUTPUT_SPLIT_SPECIALTY_OPTIONS.map((item) => ({
|
|||
|
|
key: `${item.value}Ratio`,
|
|||
|
|
label: item.label,
|
|||
|
|
percentText: formatRatioText((model as Record<string, number | undefined>)[`${item.value}Ratio`]),
|
|||
|
|
percent: toPercentValue((model as Record<string, number | undefined>)[`${item.value}Ratio`]),
|
|||
|
|
amount: amountMap[item.value]
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const projectResultRows = computed(() => buildProjectRows(formData.value))
|
|||
|
|
const specialtyResultRows = computed(() => buildSpecialtyRows(formData.value))
|
|||
|
|
const editProjectRatioItems = computed(() => buildProjectRows(editForm.value))
|
|||
|
|
const editSpecialtyRatioItems = computed(() => buildSpecialtyRows(editForm.value))
|
|||
|
|
|
|||
|
|
const annualCategoryMeta = computed(() => {
|
|||
|
|
const model = formData.value
|
|||
|
|
const currentOption =
|
|||
|
|
annualCategoryOptions.find((item) => item.value === selectedAnnualCategory.value) ||
|
|||
|
|
annualCategoryOptions[0]
|
|||
|
|
if (!model) {
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio: 0,
|
|||
|
|
percentText: '0.00%',
|
|||
|
|
totalAmount: 0,
|
|||
|
|
formulaText: '-'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const officeRatio = toNumeric(model.officeRatio)
|
|||
|
|
const officeRatioText = formatRatioText(model.officeRatio)
|
|||
|
|
switch (selectedAnnualCategory.value) {
|
|||
|
|
case 'projectManager':
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio: toNumeric(model.projectManagerRatio),
|
|||
|
|
percentText: formatRatioText(model.projectManagerRatio),
|
|||
|
|
totalAmount: toNumeric(model.projectManagerAmount),
|
|||
|
|
formulaText: `考核产值 × 项目经理比例 ${formatRatioText(model.projectManagerRatio)}`
|
|||
|
|
}
|
|||
|
|
case 'engineeringLeader':
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio: toNumeric(model.engineeringLeaderRatio),
|
|||
|
|
percentText: formatRatioText(model.engineeringLeaderRatio),
|
|||
|
|
totalAmount: toNumeric(model.engineeringLeaderAmount),
|
|||
|
|
formulaText: `考核产值 × 项目负责人比例 ${formatRatioText(model.engineeringLeaderRatio)}`
|
|||
|
|
}
|
|||
|
|
case 'arch': {
|
|||
|
|
const ratio = Number((officeRatio * toNumeric(model.archRatio)).toFixed(4))
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio,
|
|||
|
|
percentText: formatRatioText(ratio),
|
|||
|
|
totalAmount: toNumeric(model.archAmount),
|
|||
|
|
formulaText: `考核产值 × 专业所比例 ${officeRatioText} × 建筑专业比例 ${formatRatioText(model.archRatio)}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
case 'decor': {
|
|||
|
|
const ratio = Number((officeRatio * toNumeric(model.decorRatio)).toFixed(4))
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio,
|
|||
|
|
percentText: formatRatioText(ratio),
|
|||
|
|
totalAmount: toNumeric(model.decorAmount),
|
|||
|
|
formulaText: `考核产值 × 专业所比例 ${officeRatioText} × 装修专业比例 ${formatRatioText(model.decorRatio)}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
case 'struct': {
|
|||
|
|
const ratio = Number((officeRatio * toNumeric(model.structRatio)).toFixed(4))
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio,
|
|||
|
|
percentText: formatRatioText(ratio),
|
|||
|
|
totalAmount: toNumeric(model.structAmount),
|
|||
|
|
formulaText: `考核产值 × 专业所比例 ${officeRatioText} × 结构专业比例 ${formatRatioText(model.structRatio)}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
case 'water': {
|
|||
|
|
const ratio = Number((officeRatio * toNumeric(model.waterRatio)).toFixed(4))
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio,
|
|||
|
|
percentText: formatRatioText(ratio),
|
|||
|
|
totalAmount: toNumeric(model.waterAmount),
|
|||
|
|
formulaText: `考核产值 × 专业所比例 ${officeRatioText} × 水专业比例 ${formatRatioText(model.waterRatio)}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
case 'elec': {
|
|||
|
|
const ratio = Number((officeRatio * toNumeric(model.elecRatio)).toFixed(4))
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio,
|
|||
|
|
percentText: formatRatioText(ratio),
|
|||
|
|
totalAmount: toNumeric(model.elecAmount),
|
|||
|
|
formulaText: `考核产值 × 专业所比例 ${officeRatioText} × 电气专业比例 ${formatRatioText(model.elecRatio)}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
case 'hvac': {
|
|||
|
|
const ratio = Number((officeRatio * toNumeric(model.hvacRatio)).toFixed(4))
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio,
|
|||
|
|
percentText: formatRatioText(ratio),
|
|||
|
|
totalAmount: toNumeric(model.hvacAmount),
|
|||
|
|
formulaText: `考核产值 × 专业所比例 ${officeRatioText} × 暖通专业比例 ${formatRatioText(model.hvacRatio)}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
case 'digital': {
|
|||
|
|
const ratio = Number((officeRatio * toNumeric(model.digitalRatio)).toFixed(4))
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio,
|
|||
|
|
percentText: formatRatioText(ratio),
|
|||
|
|
totalAmount: toNumeric(model.digitalAmount),
|
|||
|
|
formulaText: `考核产值 × 专业所比例 ${officeRatioText} × 数字化设计专业比例 ${formatRatioText(model.digitalRatio)}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
default:
|
|||
|
|
return {
|
|||
|
|
label: currentOption.label,
|
|||
|
|
ratio: 0,
|
|||
|
|
percentText: '0.00%',
|
|||
|
|
totalAmount: 0,
|
|||
|
|
formulaText: '-'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const annualDistributionRows = computed(() =>
|
|||
|
|
quarterRows.value.map((row) => {
|
|||
|
|
const quarterAmounts: Record<number, number> = {}
|
|||
|
|
let yearTotal = 0
|
|||
|
|
QUARTER_OPTIONS.forEach((quarter) => {
|
|||
|
|
const quarterNo = Number(quarter.value)
|
|||
|
|
const quarterAmount = multiplyAmount(
|
|||
|
|
row.quarters.find((item) => item.quarterNo === quarterNo)?.distributionAmount,
|
|||
|
|
annualCategoryMeta.value.ratio
|
|||
|
|
)
|
|||
|
|
quarterAmounts[quarterNo] = quarterAmount
|
|||
|
|
yearTotal += quarterAmount
|
|||
|
|
})
|
|||
|
|
return {
|
|||
|
|
distributionYear: row.distributionYear,
|
|||
|
|
quarterAmounts,
|
|||
|
|
yearTotal: Number(yearTotal.toFixed(2))
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const annualSummaryCards = computed(() => [
|
|||
|
|
{
|
|||
|
|
label: '总分配',
|
|||
|
|
amount: multiplyAmount(
|
|||
|
|
currentPlanning.value?.assessmentOutputValue,
|
|||
|
|
currentPlanning.value?.totalDistributionAmount,
|
|||
|
|
annualCategoryMeta.value.ratio
|
|||
|
|
),
|
|||
|
|
percentText: formatRatioText(currentPlanning.value?.totalDistributionAmount)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: '已分配',
|
|||
|
|
amount: multiplyAmount(
|
|||
|
|
currentPlanning.value?.assessmentOutputValue,
|
|||
|
|
currentPlanning.value?.allocatedAmount,
|
|||
|
|
annualCategoryMeta.value.ratio
|
|||
|
|
),
|
|||
|
|
percentText: formatRatioText(currentPlanning.value?.allocatedAmount)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: '待分配',
|
|||
|
|
amount: multiplyAmount(
|
|||
|
|
currentPlanning.value?.assessmentOutputValue,
|
|||
|
|
currentPlanning.value?.pendingAmount,
|
|||
|
|
annualCategoryMeta.value.ratio
|
|||
|
|
),
|
|||
|
|
percentText: formatRatioText(currentPlanning.value?.pendingAmount)
|
|||
|
|
}
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
const projectRatioTotal = computed(() =>
|
|||
|
|
editProjectRatioItems.value.reduce((sum, item) => sum + Number(item.percent || 0), 0)
|
|||
|
|
)
|
|||
|
|
const specialtyRatioTotal = computed(() =>
|
|||
|
|
editSpecialtyRatioItems.value.reduce((sum, item) => sum + Number(item.percent || 0), 0)
|
|||
|
|
)
|
|||
|
|
const projectRatioTotalText = computed(() => `${projectRatioTotal.value.toFixed(2)}%`)
|
|||
|
|
const specialtyRatioTotalText = computed(() => `${specialtyRatioTotal.value.toFixed(2)}%`)
|
|||
|
|
const projectRatioTotalClass = computed(() =>
|
|||
|
|
Math.abs(projectRatioTotal.value - 100) < 0.001
|
|||
|
|
? 'text-[var(--el-color-success)]'
|
|||
|
|
: 'text-[var(--el-color-danger)]'
|
|||
|
|
)
|
|||
|
|
const specialtyRatioTotalClass = computed(() =>
|
|||
|
|
Math.abs(specialtyRatioTotal.value - 100) < 0.001
|
|||
|
|
? 'text-[var(--el-color-success)]'
|
|||
|
|
: 'text-[var(--el-color-danger)]'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const recalculateOfficeRatio = () => {
|
|||
|
|
if (!editForm.value) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
editForm.value.officeRatio = Number(
|
|||
|
|
Math.max(
|
|||
|
|
0,
|
|||
|
|
1 - Number(editForm.value.projectManagerRatio || 0) - Number(editForm.value.engineeringLeaderRatio || 0)
|
|||
|
|
).toFixed(4)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const recalculateArchRatio = () => {
|
|||
|
|
if (!editForm.value) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const otherSpecialtyTotal =
|
|||
|
|
Number(editForm.value.decorRatio || 0) +
|
|||
|
|
Number(editForm.value.structRatio || 0) +
|
|||
|
|
Number(editForm.value.waterRatio || 0) +
|
|||
|
|
Number(editForm.value.elecRatio || 0) +
|
|||
|
|
Number(editForm.value.hvacRatio || 0) +
|
|||
|
|
Number(editForm.value.digitalRatio || 0)
|
|||
|
|
editForm.value.archRatio = Number(Math.max(0, 1 - otherSpecialtyTotal).toFixed(4))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const buildQuarterRows = (
|
|||
|
|
planning: PlanningApi.ProjectPlanningVO,
|
|||
|
|
quarters: PlanningQuarterApi.ProjectPlanningQuarterVO[]
|
|||
|
|
) => {
|
|||
|
|
const yearSet = new Set<number>()
|
|||
|
|
if (planning.planningStartYear) {
|
|||
|
|
yearSet.add(planning.planningStartYear)
|
|||
|
|
}
|
|||
|
|
quarters.forEach((item) => yearSet.add(item.distributionYear))
|
|||
|
|
if (yearSet.size === 0) {
|
|||
|
|
yearSet.add(new Date().getFullYear())
|
|||
|
|
}
|
|||
|
|
return Array.from(yearSet)
|
|||
|
|
.sort((a, b) => a - b)
|
|||
|
|
.map((distributionYear) => ({
|
|||
|
|
distributionYear,
|
|||
|
|
quarters: QUARTER_OPTIONS.map((option) => {
|
|||
|
|
const quarterNo = Number(option.value)
|
|||
|
|
const match = quarters.find(
|
|||
|
|
(item) =>
|
|||
|
|
item.distributionYear === distributionYear && Number(item.quarterNo) === quarterNo
|
|||
|
|
)
|
|||
|
|
return (
|
|||
|
|
match || {
|
|||
|
|
planningId: planning.id!,
|
|||
|
|
distributionYear,
|
|||
|
|
quarterNo,
|
|||
|
|
distributionRatio: undefined,
|
|||
|
|
distributionAmount: 0
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getProjectList = async () => {
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const data = await ProjectApi.getProjectPage(queryParams)
|
|||
|
|
projectList.value = data.list
|
|||
|
|
total.value = data.total
|
|||
|
|
if (!projectList.value.length) {
|
|||
|
|
currentProject.value = undefined
|
|||
|
|
planningList.value = []
|
|||
|
|
currentPlanning.value = undefined
|
|||
|
|
formData.value = undefined
|
|||
|
|
quarterRows.value = []
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const targetProjectId = currentProject.value?.id || projectList.value[0].id
|
|||
|
|
const targetProject =
|
|||
|
|
projectList.value.find((item) => item.id === targetProjectId) || projectList.value[0]
|
|||
|
|
await nextTick()
|
|||
|
|
projectTableRef.value?.setCurrentRow(targetProject)
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getPlanningList = async () => {
|
|||
|
|
if (!currentProject.value?.id) {
|
|||
|
|
planningList.value = []
|
|||
|
|
currentPlanning.value = undefined
|
|||
|
|
formData.value = undefined
|
|||
|
|
quarterRows.value = []
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
planningLoading.value = true
|
|||
|
|
try {
|
|||
|
|
const list = await PlanningApi.getProjectPlanningListByProjectId(currentProject.value.id)
|
|||
|
|
planningList.value = list.filter((item) => isMajorOwnership(item.ownershipType))
|
|||
|
|
if (!planningList.value.length) {
|
|||
|
|
currentPlanning.value = undefined
|
|||
|
|
formData.value = undefined
|
|||
|
|
quarterRows.value = []
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const targetPlanningId = currentPlanning.value?.id || planningList.value[0].id
|
|||
|
|
const targetPlanning =
|
|||
|
|
planningList.value.find((item) => item.id === targetPlanningId) || planningList.value[0]
|
|||
|
|
await nextTick()
|
|||
|
|
planningTableRef.value?.setCurrentRow(targetPlanning)
|
|||
|
|
} finally {
|
|||
|
|
planningLoading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const loadPlanningRelatedData = async (planning: PlanningApi.ProjectPlanningVO) => {
|
|||
|
|
if (!planning.id) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
quarterLoading.value = true
|
|||
|
|
try {
|
|||
|
|
const [planningDetail, outputSplit, quarterList] = await Promise.all([
|
|||
|
|
PlanningApi.getProjectPlanning(planning.id),
|
|||
|
|
OutputSplitApi.getProjectOutputSplitByPlanningId(planning.id),
|
|||
|
|
PlanningQuarterApi.getProjectPlanningQuarterListByPlanningId(planning.id)
|
|||
|
|
])
|
|||
|
|
currentPlanning.value = planningDetail
|
|||
|
|
formData.value = outputSplit
|
|||
|
|
quarterRows.value = buildQuarterRows(planningDetail, quarterList)
|
|||
|
|
} finally {
|
|||
|
|
quarterLoading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleQuery = () => {
|
|||
|
|
queryParams.pageNo = 1
|
|||
|
|
getProjectList()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const resetQuery = () => {
|
|||
|
|
queryFormRef.value?.resetFields()
|
|||
|
|
handleQuery()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleCurrentProjectChange = async (row?: ProjectApi.ProjectVO) => {
|
|||
|
|
currentProject.value = row || undefined
|
|||
|
|
await getPlanningList()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleCurrentPlanningChange = async (row?: PlanningApi.ProjectPlanningVO) => {
|
|||
|
|
currentPlanning.value = row || undefined
|
|||
|
|
if (!row?.id) {
|
|||
|
|
formData.value = undefined
|
|||
|
|
quarterRows.value = []
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
await loadPlanningRelatedData(row)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const refreshCurrentPlanning = async () => {
|
|||
|
|
if (!currentPlanning.value?.id) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
await loadPlanningRelatedData(currentPlanning.value)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const openEditDialog = () => {
|
|||
|
|
if (!formData.value) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
editForm.value = { ...formData.value }
|
|||
|
|
dialogVisible.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateProjectRatio = (key: string, value?: number) => {
|
|||
|
|
if (!editForm.value || key === 'officeRatio') {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
;(editForm.value as Record<string, number | undefined>)[key] = fromPercentValue(value) ?? 0
|
|||
|
|
recalculateOfficeRatio()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateSpecialtyRatio = (key: string, value?: number) => {
|
|||
|
|
if (!editForm.value || key === 'archRatio') {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
;(editForm.value as Record<string, number | undefined>)[key] = fromPercentValue(value) ?? 0
|
|||
|
|
recalculateArchRatio()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSave = async () => {
|
|||
|
|
if (!editForm.value) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (Math.abs(projectRatioTotal.value - 100) >= 0.001) {
|
|||
|
|
message.warning('项目层比例合计必须等于100%')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (Math.abs(specialtyRatioTotal.value - 100) >= 0.001) {
|
|||
|
|
message.warning('专业层比例合计必须等于100%')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
saveLoading.value = true
|
|||
|
|
try {
|
|||
|
|
await OutputSplitApi.saveProjectOutputSplit({
|
|||
|
|
planningId: editForm.value.planningId,
|
|||
|
|
projectManagerRatio: editForm.value.projectManagerRatio,
|
|||
|
|
engineeringLeaderRatio: editForm.value.engineeringLeaderRatio,
|
|||
|
|
officeRatio: editForm.value.officeRatio,
|
|||
|
|
archRatio: editForm.value.archRatio,
|
|||
|
|
decorRatio: editForm.value.decorRatio,
|
|||
|
|
structRatio: editForm.value.structRatio,
|
|||
|
|
waterRatio: editForm.value.waterRatio,
|
|||
|
|
elecRatio: editForm.value.elecRatio,
|
|||
|
|
hvacRatio: editForm.value.hvacRatio,
|
|||
|
|
digitalRatio: editForm.value.digitalRatio
|
|||
|
|
})
|
|||
|
|
message.success('保存成功')
|
|||
|
|
dialogVisible.value = false
|
|||
|
|
await refreshCurrentPlanning()
|
|||
|
|
} finally {
|
|||
|
|
saveLoading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
getProjectList()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onActivated(() => {
|
|||
|
|
getProjectList()
|
|||
|
|
})
|
|||
|
|
</script>
|