修改代码
This commit is contained in:
821
src/views/tjt/staff-assignment/index.vue
Normal file
821
src/views/tjt/staff-assignment/index.vue
Normal file
@@ -0,0 +1,821 @@
|
||||
<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 && currentSpecialtyGroup">
|
||||
<div class="mb-16px flex items-center justify-between gap-12px">
|
||||
<div>
|
||||
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-12px">
|
||||
<el-button
|
||||
v-hasPermi="['tjt:specialty-role-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>
|
||||
|
||||
<div class="mb-16px flex flex-wrap items-start justify-between gap-12px">
|
||||
<el-radio-group v-model="selectedSpecialtyCode" size="small">
|
||||
<el-radio-button
|
||||
v-for="item in specialtyTabOptions"
|
||||
: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 currentSpecialtySummaryCards" :key="item.label" :span="6">
|
||||
<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">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="currentSpecialtyGroup.rows" border>
|
||||
<el-table-column align="center" label="角色" min-width="110" prop="roleName" />
|
||||
<el-table-column align="center" label="角色比例" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatPercentText(scope.row.roleRatio) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="角色金额(元)" min-width="130">
|
||||
<template #default="scope">
|
||||
{{ formatAmountText(scope.row.roleAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="人员合计" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatPercentText(getPersonTotalRatio(scope.row)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="人员信息" min-width="360">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.persons?.length" class="person-display-list">
|
||||
<div
|
||||
v-for="(person, index) in scope.row.persons"
|
||||
:key="`${scope.row.specialtyCode}-${scope.row.roleCode}-display-${index}`"
|
||||
class="person-display-row"
|
||||
>
|
||||
<span class="person-display-name">{{ person.personName || '-' }}</span>
|
||||
<span>{{ formatPercentText(person.personRatio) }}</span>
|
||||
<span>{{ formatAmountText(person.personAmount) }} 元</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="person-empty">暂无人员</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</ContentWrap>
|
||||
|
||||
<ContentWrap v-else>
|
||||
<el-empty description="请选择专业所规划后查看页面5结果" />
|
||||
</ContentWrap>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<Dialog v-model="dialogVisible" title="编辑专业人员角色分配" width="1180">
|
||||
<template v-if="currentEditSpecialtyGroup">
|
||||
|
||||
<div class="mb-16px flex flex-wrap items-start justify-between gap-12px">
|
||||
<el-radio-group v-model="editSpecialtyCode" size="small">
|
||||
<el-radio-button
|
||||
v-for="item in specialtyTabOptions"
|
||||
:key="`edit-${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 currentEditSpecialtySummaryCards" :key="item.label" :span="6">
|
||||
<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">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="currentEditSpecialtyGroup.rows" border>
|
||||
<el-table-column align="center" label="角色" min-width="110" prop="roleName" />
|
||||
<el-table-column align="center" label="角色比例" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-input-number
|
||||
:model-value="toPercentValue(scope.row.roleRatio)"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="!w-1/1"
|
||||
controls-position="right"
|
||||
@update:model-value="(value) => updateRoleRatio(scope.row, value)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="角色金额(元)" min-width="130">
|
||||
<template #default="scope">
|
||||
{{ formatAmountText(scope.row.roleAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="人员合计" min-width="120">
|
||||
<template #default="scope">
|
||||
<span :class="getPersonTotalClass(scope.row)">
|
||||
{{ formatPercentText(getPersonTotalRatio(scope.row)) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人员配置" min-width="520">
|
||||
<template #default="scope">
|
||||
<div class="person-editor">
|
||||
<div v-if="!scope.row.persons?.length" class="person-empty">
|
||||
暂无人员,请点击“添加人员”
|
||||
</div>
|
||||
<div
|
||||
v-for="(person, index) in scope.row.persons"
|
||||
:key="`${scope.row.specialtyCode}-${scope.row.roleCode}-${index}`"
|
||||
class="person-row"
|
||||
>
|
||||
<el-input
|
||||
v-model="person.personName"
|
||||
clearable
|
||||
maxlength="64"
|
||||
placeholder="请输入人员名称"
|
||||
/>
|
||||
<el-input-number
|
||||
:model-value="toPercentValue(person.personRatio)"
|
||||
:max="getPersonRatioMax(scope.row, person)"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="!w-1/1"
|
||||
controls-position="right"
|
||||
placeholder="比例(%)"
|
||||
@update:model-value="(value) => updatePersonRatio(scope.row, person, value)"
|
||||
/>
|
||||
<div class="person-amount">{{ formatAmountText(person.personAmount) }} 元</div>
|
||||
<el-button text type="danger" @click="removePerson(scope.row, index)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="person-toolbar">
|
||||
<el-button text type="primary" @click="addPerson(scope.row)">
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
添加人员
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</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 SpecialtyRoleSplitApi from '@/api/tjt/specialtyRoleSplit'
|
||||
import {
|
||||
formatAmountText,
|
||||
formatPercentText,
|
||||
fromPercentValue,
|
||||
isMajorOwnership,
|
||||
OUTPUT_SPLIT_SPECIALTY_OPTIONS,
|
||||
toPercentValue
|
||||
} from '@/views/tjt/shared/planning'
|
||||
|
||||
defineOptions({ name: 'TjtStaffAssignment' })
|
||||
|
||||
const DESIGN_ROLE_CODE = 'design'
|
||||
const EPSILON = 0.0001
|
||||
|
||||
type SpecialtyRolePersonVO = SpecialtyRoleSplitApi.SpecialtyRolePersonVO
|
||||
type SpecialtyRoleSplitVO = SpecialtyRoleSplitApi.SpecialtyRoleSplitVO
|
||||
|
||||
interface SpecialtyGroup {
|
||||
specialtyCode: string
|
||||
specialtyName: string
|
||||
specialtyAmount: number
|
||||
rows: SpecialtyRoleSplitVO[]
|
||||
roleTotal: number
|
||||
designRatio: number
|
||||
personCount: number
|
||||
}
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(false)
|
||||
const planningLoading = 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 roleList = ref<SpecialtyRoleSplitVO[]>([])
|
||||
const editRoleList = ref<SpecialtyRoleSplitVO[]>([])
|
||||
const selectedSpecialtyCode = ref('')
|
||||
const editSpecialtyCode = ref('')
|
||||
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 roundRatio = (value?: number | string | null) => {
|
||||
const numericValue = Number(value || 0)
|
||||
if (Number.isNaN(numericValue)) {
|
||||
return 0
|
||||
}
|
||||
return Number(numericValue.toFixed(4))
|
||||
}
|
||||
|
||||
const roundAmount = (value?: number | string | null) => {
|
||||
const numericValue = Number(value || 0)
|
||||
if (Number.isNaN(numericValue)) {
|
||||
return 0
|
||||
}
|
||||
return Number(numericValue.toFixed(2))
|
||||
}
|
||||
|
||||
const multiplyAmount = (amount?: number | string | null, ratio?: number | string | null) => {
|
||||
return roundAmount(Number(amount || 0) * Number(ratio || 0))
|
||||
}
|
||||
|
||||
const normalizePersonName = (value?: string) => value?.trim() || ''
|
||||
|
||||
const buildEditablePerson = (person?: SpecialtyRolePersonVO): SpecialtyRolePersonVO => ({
|
||||
personName: person?.personName || '',
|
||||
personRatio:
|
||||
person?.personRatio === undefined || person?.personRatio === null
|
||||
? undefined
|
||||
: roundRatio(person.personRatio),
|
||||
personAmount: roundAmount(person?.personAmount)
|
||||
})
|
||||
|
||||
const buildEditableRow = (row: SpecialtyRoleSplitVO): SpecialtyRoleSplitVO => ({
|
||||
...row,
|
||||
specialtyAmount: roundAmount(row.specialtyAmount),
|
||||
roleRatio: roundRatio(row.roleRatio),
|
||||
roleAmount: roundAmount(row.roleAmount),
|
||||
persons: (row.persons || []).map((person) => buildEditablePerson(person))
|
||||
})
|
||||
|
||||
const cloneRoleRows = (rows: SpecialtyRoleSplitVO[]) => rows.map((item) => buildEditableRow(item))
|
||||
|
||||
const sumPersonRatios = (persons?: SpecialtyRolePersonVO[]) =>
|
||||
roundRatio((persons || []).reduce((sum, person) => sum + Number(person?.personRatio || 0), 0))
|
||||
|
||||
const getPersonTotalRatio = (row: SpecialtyRoleSplitVO) => sumPersonRatios(row.persons)
|
||||
|
||||
const getPersonTotalClass = (row: SpecialtyRoleSplitVO) =>
|
||||
getPersonTotalRatio(row) > 1 + EPSILON ? 'text-[var(--el-color-danger)]' : ''
|
||||
|
||||
const getPersonRatioMax = (row: SpecialtyRoleSplitVO, currentPerson: SpecialtyRolePersonVO) => {
|
||||
const otherTotalRatio = roundRatio(
|
||||
(row.persons || []).reduce((sum, person) => {
|
||||
if (person === currentPerson) {
|
||||
return sum
|
||||
}
|
||||
return sum + Number(person?.personRatio || 0)
|
||||
}, 0)
|
||||
)
|
||||
const currentPercent = toPercentValue(currentPerson.personRatio) ?? 0
|
||||
const remainPercent = toPercentValue(Math.max(0, 1 - otherTotalRatio)) ?? 0
|
||||
return Math.min(100, Math.max(currentPercent, remainPercent))
|
||||
}
|
||||
|
||||
const countConfiguredPersons = (persons?: SpecialtyRolePersonVO[]) =>
|
||||
(persons || []).filter(
|
||||
(person) =>
|
||||
normalizePersonName(person.personName) ||
|
||||
(person.personRatio !== undefined && person.personRatio !== null)
|
||||
).length
|
||||
|
||||
const syncSpecialtyRows = (source: SpecialtyRoleSplitVO[], specialtyCode: string) => {
|
||||
const rows = source.filter((item) => item.specialtyCode === specialtyCode)
|
||||
rows.forEach((row) => {
|
||||
row.persons = (row.persons || []).map((person) => buildEditablePerson(person))
|
||||
row.roleRatio = roundRatio(row.roleRatio)
|
||||
row.roleAmount = multiplyAmount(row.specialtyAmount, row.roleRatio)
|
||||
row.persons = (row.persons || []).map((person) => ({
|
||||
...person,
|
||||
personAmount: multiplyAmount(row.roleAmount, person.personRatio)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const syncAllDerivedValues = (source: SpecialtyRoleSplitVO[]) => {
|
||||
const specialtyCodeSet = new Set(source.map((item) => item.specialtyCode))
|
||||
specialtyCodeSet.forEach((specialtyCode) => syncSpecialtyRows(source, specialtyCode))
|
||||
}
|
||||
|
||||
const buildSpecialtyGroups = (source: SpecialtyRoleSplitVO[]): SpecialtyGroup[] => {
|
||||
const map = new Map<string, SpecialtyRoleSplitVO[]>()
|
||||
source.forEach((item) => {
|
||||
const list = map.get(item.specialtyCode) || []
|
||||
list.push(item)
|
||||
map.set(item.specialtyCode, list)
|
||||
})
|
||||
return OUTPUT_SPLIT_SPECIALTY_OPTIONS.map((option) => {
|
||||
const rows = map.get(option.value)
|
||||
if (!rows?.length) {
|
||||
return undefined
|
||||
}
|
||||
const sortedRows = [...rows].sort((a, b) => Number(a.sortNo || 0) - Number(b.sortNo || 0))
|
||||
const specialtyName = sortedRows[0]?.specialtyName || option.label
|
||||
const specialtyAmount = roundAmount(sortedRows[0]?.specialtyAmount)
|
||||
const roleTotal = roundRatio(
|
||||
sortedRows.reduce((sum, item) => sum + Number(item.roleRatio || 0), 0)
|
||||
)
|
||||
const designRow = sortedRows.find((item) => item.roleCode === DESIGN_ROLE_CODE)
|
||||
const personCount = sortedRows.reduce(
|
||||
(sum, row) => sum + countConfiguredPersons(row.persons),
|
||||
0
|
||||
)
|
||||
return {
|
||||
specialtyCode: option.value,
|
||||
specialtyName,
|
||||
specialtyAmount,
|
||||
rows: sortedRows,
|
||||
roleTotal,
|
||||
designRatio: roundRatio(designRow?.roleRatio),
|
||||
personCount
|
||||
}
|
||||
}).filter((item): item is SpecialtyGroup => !!item)
|
||||
}
|
||||
|
||||
const specialtyGroups = computed(() => buildSpecialtyGroups(roleList.value))
|
||||
const editSpecialtyGroups = computed(() => buildSpecialtyGroups(editRoleList.value))
|
||||
const specialtyTabOptions = computed(() =>
|
||||
specialtyGroups.value.map((item) => ({
|
||||
label: item.specialtyName,
|
||||
value: item.specialtyCode
|
||||
}))
|
||||
)
|
||||
|
||||
const currentSpecialtyGroup = computed(
|
||||
() =>
|
||||
specialtyGroups.value.find((item) => item.specialtyCode === selectedSpecialtyCode.value) ||
|
||||
specialtyGroups.value[0]
|
||||
)
|
||||
|
||||
const currentEditSpecialtyGroup = computed(
|
||||
() =>
|
||||
editSpecialtyGroups.value.find((item) => item.specialtyCode === editSpecialtyCode.value) ||
|
||||
editSpecialtyGroups.value[0]
|
||||
)
|
||||
|
||||
const buildSpecialtySummaryCards = (group?: SpecialtyGroup) => {
|
||||
if (!group) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{ label: '专业金额', value: `${formatAmountText(group.specialtyAmount)} 元` },
|
||||
{ label: '角色合计', value: formatPercentText(group.roleTotal) },
|
||||
{ label: '设计比例', value: formatPercentText(group.designRatio) },
|
||||
{ label: '已配置人数', value: `${group.personCount} 人` }
|
||||
]
|
||||
}
|
||||
|
||||
const currentSpecialtySummaryCards = computed(() =>
|
||||
buildSpecialtySummaryCards(currentSpecialtyGroup.value)
|
||||
)
|
||||
const currentEditSpecialtySummaryCards = computed(() =>
|
||||
buildSpecialtySummaryCards(currentEditSpecialtyGroup.value)
|
||||
)
|
||||
|
||||
const ensureSelectedCode = (codes: string[], target: { value: string }) => {
|
||||
if (!codes.length) {
|
||||
target.value = ''
|
||||
return
|
||||
}
|
||||
if (!codes.includes(target.value)) {
|
||||
target.value = codes[0]
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
specialtyGroups,
|
||||
(groups) => {
|
||||
ensureSelectedCode(
|
||||
groups.map((item) => item.specialtyCode),
|
||||
selectedSpecialtyCode
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
editSpecialtyGroups,
|
||||
(groups) => {
|
||||
ensureSelectedCode(
|
||||
groups.map((item) => item.specialtyCode),
|
||||
editSpecialtyCode
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const addPerson = (row: SpecialtyRoleSplitVO) => {
|
||||
if (!row.persons) {
|
||||
row.persons = []
|
||||
}
|
||||
row.persons.push(buildEditablePerson())
|
||||
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
|
||||
}
|
||||
|
||||
const removePerson = (row: SpecialtyRoleSplitVO, index: number) => {
|
||||
row.persons?.splice(index, 1)
|
||||
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
|
||||
}
|
||||
|
||||
const updatePersonRatio = (
|
||||
row: SpecialtyRoleSplitVO,
|
||||
person: SpecialtyRolePersonVO,
|
||||
value?: number
|
||||
) => {
|
||||
const maxValue = getPersonRatioMax(row, person)
|
||||
const nextValue =
|
||||
value === undefined || value === null ? undefined : Math.min(Math.max(value, 0), maxValue)
|
||||
person.personRatio = fromPercentValue(nextValue) ?? undefined
|
||||
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
|
||||
}
|
||||
|
||||
const updateRoleRatio = (row: SpecialtyRoleSplitVO, value?: number) => {
|
||||
row.roleRatio = fromPercentValue(value) ?? 0
|
||||
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
|
||||
}
|
||||
|
||||
const buildSavePersons = (
|
||||
row: SpecialtyRoleSplitVO
|
||||
):
|
||||
| { persons: SpecialtyRolePersonVO[]; error?: never }
|
||||
| { persons?: never; error: string } => {
|
||||
const persons: SpecialtyRolePersonVO[] = []
|
||||
for (const person of row.persons || []) {
|
||||
const personName = normalizePersonName(person.personName)
|
||||
const hasName = !!personName
|
||||
const hasRatio = person.personRatio !== undefined && person.personRatio !== null
|
||||
if (!hasName && !hasRatio) {
|
||||
continue
|
||||
}
|
||||
if (!hasName || !hasRatio) {
|
||||
return {
|
||||
error: `${row.specialtyName || row.specialtyCode}-${row.roleName || row.roleCode}的人员名称和比例必须同时填写`
|
||||
}
|
||||
}
|
||||
persons.push({
|
||||
personName,
|
||||
personRatio: roundRatio(person.personRatio)
|
||||
})
|
||||
}
|
||||
return { persons }
|
||||
}
|
||||
|
||||
const validateAndBuildSaveItems = () => {
|
||||
const items: SpecialtyRoleSplitApi.SpecialtyRoleSplitSaveItemVO[] = []
|
||||
for (const group of editSpecialtyGroups.value) {
|
||||
let roleTotal = 0
|
||||
for (const row of group.rows) {
|
||||
const result = buildSavePersons(row)
|
||||
if (result.error) {
|
||||
message.warning(result.error)
|
||||
return undefined
|
||||
}
|
||||
const persons = result.persons || []
|
||||
const personTotalRatio = sumPersonRatios(persons)
|
||||
if (personTotalRatio > 1 + EPSILON) {
|
||||
message.warning(
|
||||
`${row.specialtyName || row.specialtyCode}-${row.roleName || row.roleCode}的人员比例合计不能大于 100%`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
if ((row.roleCode === DESIGN_ROLE_CODE && Number(row.roleAmount || 0) > EPSILON) && persons.length === 0) {
|
||||
message.warning(`${group.specialtyName}的设计角色金额大于 0 时必须至少配置 1 个人员`)
|
||||
return undefined
|
||||
}
|
||||
roleTotal = roundRatio(roleTotal + Number(row.roleRatio || 0))
|
||||
items.push({
|
||||
specialtyCode: row.specialtyCode,
|
||||
roleCode: row.roleCode,
|
||||
roleRatio: roundRatio(row.roleRatio),
|
||||
persons
|
||||
})
|
||||
}
|
||||
if (Math.abs(roleTotal - 1) > EPSILON) {
|
||||
message.warning(`${group.specialtyName}五类角色比例合计必须等于 100%`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
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
|
||||
roleList.value = []
|
||||
editRoleList.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
|
||||
roleList.value = []
|
||||
editRoleList.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
|
||||
roleList.value = []
|
||||
editRoleList.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 loadRoleList = async (planningId: number) => {
|
||||
const data = await SpecialtyRoleSplitApi.getSpecialtyRoleSplitListByPlanningId(planningId)
|
||||
roleList.value = cloneRoleRows(data)
|
||||
syncAllDerivedValues(roleList.value)
|
||||
}
|
||||
|
||||
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) {
|
||||
roleList.value = []
|
||||
editRoleList.value = []
|
||||
return
|
||||
}
|
||||
await loadRoleList(row.id)
|
||||
}
|
||||
|
||||
const refreshCurrentPlanning = async () => {
|
||||
if (!currentPlanning.value?.id) {
|
||||
return
|
||||
}
|
||||
await loadRoleList(currentPlanning.value.id)
|
||||
}
|
||||
|
||||
const openEditDialog = () => {
|
||||
if (!roleList.value.length) {
|
||||
return
|
||||
}
|
||||
editRoleList.value = cloneRoleRows(roleList.value)
|
||||
syncAllDerivedValues(editRoleList.value)
|
||||
editSpecialtyCode.value = selectedSpecialtyCode.value || specialtyTabOptions.value[0]?.value || ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!currentPlanning.value?.id) {
|
||||
return
|
||||
}
|
||||
const items = validateAndBuildSaveItems()
|
||||
if (!items) {
|
||||
return
|
||||
}
|
||||
saveLoading.value = true
|
||||
try {
|
||||
await SpecialtyRoleSplitApi.saveSpecialtyRoleSplitBatch({
|
||||
planningId: currentPlanning.value.id,
|
||||
items
|
||||
})
|
||||
message.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
await loadRoleList(currentPlanning.value.id)
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getProjectList()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
getProjectList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.person-empty {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.person-display-list,
|
||||
.person-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.person-display-row,
|
||||
.person-row {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.person-display-row {
|
||||
grid-template-columns: minmax(120px, 1fr) 100px 120px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.person-display-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.person-row {
|
||||
grid-template-columns: minmax(160px, 1fr) 180px 120px 64px;
|
||||
}
|
||||
|
||||
.person-amount {
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.person-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.person-row {
|
||||
grid-template-columns: minmax(140px, 1fr) 160px 110px 64px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user