0513新功能优化

This commit is contained in:
lzm
2026-05-13 11:41:30 +08:00
parent ddc0c7db1e
commit 5cb913cb0a
20 changed files with 442 additions and 229 deletions

View File

@@ -1,4 +1,6 @@
import request from '@/config/axios' import request from '@/config/axios'
import type { ProjectPlanningVO } from '@/api/tjt/planning'
import type { ProjectPlanningQuarterVO } from '@/api/tjt/planningQuarter'
export interface ProjectOutputSplitVO { export interface ProjectOutputSplitVO {
id?: number id?: number
@@ -45,10 +47,20 @@ export type ProjectOutputSplitSaveVO = Pick<
| 'digitalRatio' | 'digitalRatio'
> >
export interface ProjectOutputSplitPlanningDetailVO {
planning: ProjectPlanningVO
outputSplit: ProjectOutputSplitVO
quarters: ProjectPlanningQuarterVO[]
}
export const getProjectOutputSplitByPlanningId = (planningId: number) => { export const getProjectOutputSplitByPlanningId = (planningId: number) => {
return request.get({ url: '/tjt/output-split/get-by-planning', params: { planningId } }) return request.get({ url: '/tjt/output-split/get-by-planning', params: { planningId } })
} }
export const getProjectOutputSplitPlanningDetail = (planningId: number) => {
return request.get({ url: '/tjt/output-split/planning-detail', params: { planningId } })
}
export const saveProjectOutputSplit = (data: ProjectOutputSplitSaveVO) => { export const saveProjectOutputSplit = (data: ProjectOutputSplitSaveVO) => {
return request.put({ url: '/tjt/output-split/save', data }) return request.put({ url: '/tjt/output-split/save', data })
} }

View File

@@ -1,4 +1,5 @@
import request from '@/config/axios' import request from '@/config/axios'
import type { ProjectPlanningGuideDetailVO } from '@/api/tjt/planningGuideDetail'
export interface ProjectPlanningVO { export interface ProjectPlanningVO {
id?: number id?: number
@@ -71,6 +72,11 @@ export interface ProjectPlanningPageReqVO extends PageParam {
createTime?: string[] createTime?: string[]
} }
export interface ProjectPlanningOutputEditDetailVO {
planning: ProjectPlanningVO
guideDetails: ProjectPlanningGuideDetailVO[]
}
export const getProjectPlanningPage = (params: ProjectPlanningPageReqVO) => { export const getProjectPlanningPage = (params: ProjectPlanningPageReqVO) => {
return request.get({ url: '/tjt/planning/page', params }) return request.get({ url: '/tjt/planning/page', params })
} }
@@ -79,6 +85,10 @@ export const getProjectPlanning = (id: number) => {
return request.get({ url: '/tjt/planning/get', params: { id } }) return request.get({ url: '/tjt/planning/get', params: { id } })
} }
export const getProjectPlanningOutputEditDetail = (id: number) => {
return request.get({ url: '/tjt/planning/output-edit-detail', params: { id } })
}
export const getProjectPlanningListByProjectId = (projectId: number) => { export const getProjectPlanningListByProjectId = (projectId: number) => {
return request.get({ url: '/tjt/planning/list-by-project', params: { projectId } }) return request.get({ url: '/tjt/planning/list-by-project', params: { projectId } })
} }

View File

@@ -1,4 +1,5 @@
import request from '@/config/axios' import request from '@/config/axios'
import type { ProjectPlanningVO } from '@/api/tjt/planning'
export interface ProjectPlanningQuarterVO { export interface ProjectPlanningQuarterVO {
id?: number id?: number
@@ -15,6 +16,11 @@ export type ProjectPlanningQuarterSaveVO = Omit<
'distributionAmount' | 'createTime' 'distributionAmount' | 'createTime'
> >
export interface ProjectPlanningQuarterPlanningDetailVO {
planning: ProjectPlanningVO
quarters: ProjectPlanningQuarterVO[]
}
export const getProjectPlanningQuarter = (id: number) => { export const getProjectPlanningQuarter = (id: number) => {
return request.get({ url: '/tjt/planning-quarter/get', params: { id } }) return request.get({ url: '/tjt/planning-quarter/get', params: { id } })
} }
@@ -23,6 +29,13 @@ export const getProjectPlanningQuarterListByPlanningId = (planningId: number) =>
return request.get({ url: '/tjt/planning-quarter/list-by-planning', params: { planningId } }) return request.get({ url: '/tjt/planning-quarter/list-by-planning', params: { planningId } })
} }
export const getProjectPlanningQuarterPlanningDetail = (planningId: number) => {
return request.get({
url: '/tjt/planning-quarter/planning-detail',
params: { planningId }
})
}
export const createProjectPlanningQuarter = (data: ProjectPlanningQuarterSaveVO) => { export const createProjectPlanningQuarter = (data: ProjectPlanningQuarterSaveVO) => {
return request.post({ url: '/tjt/planning-quarter/create', data }) return request.post({ url: '/tjt/planning-quarter/create', data })
} }

View File

@@ -34,6 +34,7 @@ export interface SpecialtyRoleSplitSaveItemVO {
export interface SpecialtyRoleSplitBatchSaveVO { export interface SpecialtyRoleSplitBatchSaveVO {
planningId: number planningId: number
items: SpecialtyRoleSplitSaveItemVO[] items: SpecialtyRoleSplitSaveItemVO[]
temporarySave?: boolean
} }
export const getSpecialtyRoleSplitListByPlanningId = (planningId: number) => { export const getSpecialtyRoleSplitListByPlanningId = (planningId: number) => {

View File

@@ -450,11 +450,18 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getList() getList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getList() getList()
}) })
</script> </script>

View File

@@ -451,11 +451,18 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getList() getList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getList() getList()
}) })
</script> </script>

View File

@@ -504,12 +504,19 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
let activatedOnce = false
onMounted(async () => { onMounted(async () => {
await loadOfficeOptions() await loadOfficeOptions()
await getList() await getList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getList() getList()
}) })
</script> </script>

View File

@@ -236,11 +236,18 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getList() getList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getList() getList()
}) })
</script> </script>

View File

@@ -103,12 +103,17 @@
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<ContentWrap v-if="currentPlanning && formData"> <ContentWrap>
<div v-loading="quarterLoading" class="min-h-320px">
<template v-if="currentPlanning && formData">
<div class="mb-16px flex items-center justify-between gap-16px"> <div class="mb-16px flex items-center justify-between gap-16px">
<div> <div>
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div> <div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
<div class="mt-4px text-13px text-[var(--el-text-color-secondary)]"> <div class="mt-4px text-13px text-[var(--el-text-color-secondary)]">
年度{{ formData.year || '-' }}考核产值{{ formatAmountText(formData.assessmentOutputValue) }} 年度{{ formData.year || '-' }}考核产值{{
formatAmountText(formData.assessmentOutputValue)
}}
</div> </div>
</div> </div>
<div class="flex items-center gap-12px"> <div class="flex items-center gap-12px">
@@ -125,10 +130,16 @@
</div> </div>
<el-descriptions :column="2" border title="基础信息"> <el-descriptions :column="2" border title="基础信息">
<el-descriptions-item label="项目名称">{{ formData.projectName || '-' }}</el-descriptions-item> <el-descriptions-item label="项目名称">{{
<el-descriptions-item label="项目任务包">{{ formData.planningContent || '-' }}</el-descriptions-item> formData.projectName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="项目任务包">{{
formData.planningContent || '-'
}}</el-descriptions-item>
<el-descriptions-item label="工程负责人"> <el-descriptions-item label="工程负责人">
{{ getProjectLeadText(formData.projectManagerName, formData.engineeringLeaderName) }} {{
getProjectLeadText(formData.projectManagerName, formData.engineeringLeaderName)
}}
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
@@ -177,13 +188,20 @@
<el-row :gutter="16" class="mb-16px"> <el-row :gutter="16" class="mb-16px">
<el-col v-for="item in annualSummaryCards" :key="item.label" :span="8"> <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="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="text-12px text-[var(--el-text-color-secondary)]">{{
item.label
}}</div>
<div class="mt-6px text-18px font-600">{{ formatAmountText(item.amount) }}</div> <div class="mt-6px text-18px font-600">{{ formatAmountText(item.amount) }}</div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-table v-loading="quarterLoading" :data="annualDistributionRows" border> <el-table :data="annualDistributionRows" border>
<el-table-column align="center" label="分配年度" min-width="120" prop="distributionYear" /> <el-table-column
align="center"
label="分配年度"
min-width="120"
prop="distributionYear"
/>
<el-table-column <el-table-column
v-for="quarter in QUARTER_OPTIONS" v-for="quarter in QUARTER_OPTIONS"
:key="String(quarter.value)" :key="String(quarter.value)"
@@ -201,10 +219,10 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</ContentWrap> </template>
<ContentWrap v-else> <el-empty v-else-if="!quarterLoading" description="请选择合约规划后查看分配结果" />
<el-empty description="请选择合约规划后查看分配结果" /> </div>
</ContentWrap> </ContentWrap>
</el-col> </el-col>
</el-row> </el-row>
@@ -618,14 +636,12 @@ const loadPlanningRelatedData = async (planning: PlanningApi.ProjectPlanningVO)
} }
quarterLoading.value = true quarterLoading.value = true
try { try {
const [planningDetail, outputSplit, quarterList] = await Promise.all([ const detail = await OutputSplitApi.getProjectOutputSplitPlanningDetail(planning.id)
PlanningApi.getProjectPlanning(planning.id), currentPlanning.value = detail?.planning
OutputSplitApi.getProjectOutputSplitByPlanningId(planning.id), formData.value = detail?.outputSplit
PlanningQuarterApi.getProjectPlanningQuarterListByPlanningId(planning.id) quarterRows.value = detail?.planning
]) ? buildQuarterRows(detail.planning, detail.quarters || [])
currentPlanning.value = planningDetail : []
formData.value = outputSplit
quarterRows.value = buildQuarterRows(planningDetail, quarterList)
} finally { } finally {
quarterLoading.value = false quarterLoading.value = false
} }
@@ -723,11 +739,18 @@ const handleSave = async () => {
} }
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getProjectList() getProjectList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求项目与合约规划列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getProjectList() getProjectList()
}) })
</script> </script>

View File

@@ -1192,12 +1192,17 @@ const open = async (id: number) => {
dialogVisible.value = true dialogVisible.value = true
formLoading.value = true formLoading.value = true
try { try {
const data = await PlanningApi.getProjectPlanning(id) const detail = await PlanningApi.getProjectPlanningOutputEditDetail(id)
formData.value = normalizeFormData(data) if (!detail?.planning) {
formData.value = createFormData()
guideDetails.value = []
dialogVisible.value = false
message.warning('合约规划不存在或已被删除')
return
}
formData.value = normalizeFormData(detail.planning)
guideDetails.value = showGuideDetailSection.value guideDetails.value = showGuideDetailSection.value
? normalizeGuideDetailList( ? normalizeGuideDetailList(detail.guideDetails)
await PlanningGuideDetailApi.getProjectPlanningGuideDetailListByPlanningId(id)
)
: [] : []
applyCalculationRatioDefault() applyCalculationRatioDefault()
if (formData.value.reviewOutsourceRatio === undefined || formData.value.reviewOutsourceRatio === null) { if (formData.value.reviewOutsourceRatio === undefined || formData.value.reviewOutsourceRatio === null) {

View File

@@ -205,7 +205,24 @@ const open = async (id: number) => {
loading.value = true loading.value = true
deletedQuarterIds.value = [] deletedQuarterIds.value = []
try { try {
const planning = await PlanningApi.getProjectPlanning(id) const detail = await PlanningQuarterApi.getProjectPlanningQuarterPlanningDetail(id)
if (!detail?.planning) {
planningId.value = undefined
formData.value = {
projectId: 0,
ownershipType: '',
calculationMethod: '',
planningContent: '',
totalDistributionAmount: 1,
progressRemark: ''
}
quarterRows.value = []
deletedQuarterIds.value = []
dialogVisible.value = false
message.warning('合约规划不存在或已被删除')
return
}
const planning = detail.planning
formData.value = { formData.value = {
...planning, ...planning,
contractValueQuantity: planning.contractValueQuantity ?? 1, contractValueQuantity: planning.contractValueQuantity ?? 1,
@@ -214,8 +231,7 @@ const open = async (id: number) => {
totalDistributionAmount: planning.totalDistributionAmount ?? 1, totalDistributionAmount: planning.totalDistributionAmount ?? 1,
progressRemark: planning.progressRemark ?? '' progressRemark: planning.progressRemark ?? ''
} }
const quarterList = await PlanningQuarterApi.getProjectPlanningQuarterListByPlanningId(id) quarterRows.value = buildQuarterRows(formData.value, detail.quarters || [])
quarterRows.value = buildQuarterRows(formData.value, quarterList)
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@@ -166,7 +166,9 @@
</el-col> </el-col>
<el-col :span="15"> <el-col :span="15">
<ContentWrap v-if="currentPlanning"> <div v-loading="quarterLoading" class="min-h-320px">
<template v-if="currentPlanning">
<ContentWrap>
<div class="mb-16px flex items-center justify-between gap-16px"> <div class="mb-16px flex items-center justify-between gap-16px">
<div> <div>
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div> <div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
@@ -298,7 +300,7 @@
</el-descriptions> </el-descriptions>
</ContentWrap> </ContentWrap>
<ContentWrap v-if="currentPlanning"> <ContentWrap>
<div class="mb-16px flex items-center justify-between gap-16px"> <div class="mb-16px flex items-center justify-between gap-16px">
<div> <div>
<div class="text-14px font-600">季度分配</div> <div class="text-14px font-600">季度分配</div>
@@ -348,7 +350,7 @@
</el-col> </el-col>
</el-row> </el-row>
<el-table v-loading="quarterLoading" :data="quarterRows" border> <el-table :data="quarterRows" border>
<el-table-column align="center" label="分配年度" width="150" prop="distributionYear" /> <el-table-column align="center" label="分配年度" width="150" prop="distributionYear" />
<el-table-column <el-table-column
v-for="quarter in QUARTER_OPTIONS" v-for="quarter in QUARTER_OPTIONS"
@@ -366,10 +368,12 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</ContentWrap> </ContentWrap>
</template>
<ContentWrap v-else> <ContentWrap v-else-if="!quarterLoading">
<el-empty description="请选择合约规划后查看测算结果和季度分配" /> <el-empty description="请选择合约规划后查看测算结果和季度分配" />
</ContentWrap> </ContentWrap>
</div>
</el-col> </el-col>
</el-row> </el-row>
@@ -581,12 +585,11 @@ const getPlanningList = async () => {
} }
const loadPlanningDetail = async (planningId: number) => { const loadPlanningDetail = async (planningId: number) => {
const planning = await PlanningApi.getProjectPlanning(planningId)
currentPlanning.value = planning
quarterLoading.value = true quarterLoading.value = true
try { try {
const quarterList = await PlanningQuarterApi.getProjectPlanningQuarterListByPlanningId(planningId) const detail = await PlanningQuarterApi.getProjectPlanningQuarterPlanningDetail(planningId)
quarterRows.value = buildQuarterRows(planning, quarterList) currentPlanning.value = detail?.planning
quarterRows.value = detail?.planning ? buildQuarterRows(detail.planning, detail.quarters || []) : []
} finally { } finally {
quarterLoading.value = false quarterLoading.value = false
} }
@@ -643,11 +646,18 @@ const handlePlanningOutputFormSuccess = async () => {
} }
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getProjectList() getProjectList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求项目与合约规划列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getProjectList() getProjectList()
}) })
</script> </script>

View File

@@ -159,7 +159,9 @@
/> />
</ContentWrap> </ContentWrap>
<ContentWrap v-if="currentProfit"> <ContentWrap>
<div v-loading="detailLoading" class="min-h-220px">
<template v-if="currentProfit">
<div class="mb-16px flex items-center justify-between gap-16px"> <div class="mb-16px flex items-center justify-between gap-16px">
<div class="text-16px font-600">{{ currentProfit.projectName }}</div> <div class="text-16px font-600">{{ currentProfit.projectName }}</div>
<el-button plain type="primary" @click="openProfitEditDialog"> <el-button plain type="primary" @click="openProfitEditDialog">
@@ -220,10 +222,10 @@
</span> </span>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</ContentWrap> </template>
<ContentWrap v-else> <el-empty v-else-if="!detailLoading" description="请选择项目后查看盈亏详情" />
<el-empty description="请选择项目后查看盈亏详情" /> </div>
</ContentWrap> </ContentWrap>
<Dialog v-model="dialogVisible" title="编辑盈亏参数" width="520"> <Dialog v-model="dialogVisible" title="编辑盈亏参数" width="520">
@@ -286,6 +288,7 @@ const message = useMessage()
const { t } = useI18n() const { t } = useI18n()
const loading = ref(false) const loading = ref(false)
const detailLoading = ref(false)
const total = ref(0) const total = ref(0)
const list = ref<ProfitApi.ProjectProfitVO[]>([]) const list = ref<ProfitApi.ProjectProfitVO[]>([])
const currentProfit = ref<ProfitApi.ProjectProfitVO>() const currentProfit = ref<ProfitApi.ProjectProfitVO>()
@@ -337,21 +340,24 @@ const queryProjectStartYearValue = computed({
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
let targetProfit: ProfitApi.ProjectProfitVO | undefined
try { try {
const data = await ProfitApi.getProjectProfitPage(queryParams) const data = await ProfitApi.getProjectProfitPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
if (!list.value.length) { if (!list.value.length) {
currentProfit.value = undefined currentProfit.value = undefined
return } else {
}
const targetProjectId = currentProfit.value?.projectId || list.value[0].projectId const targetProjectId = currentProfit.value?.projectId || list.value[0].projectId
const targetProfit = list.value.find((item) => item.projectId === targetProjectId) || list.value[0] targetProfit = list.value.find((item) => item.projectId === targetProjectId) || list.value[0]
await nextTick() }
profitTableRef.value?.setCurrentRow(targetProfit)
} finally { } finally {
loading.value = false loading.value = false
} }
if (targetProfit) {
await nextTick()
profitTableRef.value?.setCurrentRow(targetProfit)
}
} }
const handleQuery = () => { const handleQuery = () => {
@@ -369,14 +375,24 @@ const handleCurrentProfitChange = async (row?: ProfitApi.ProjectProfitVO) => {
currentProfit.value = undefined currentProfit.value = undefined
return return
} }
detailLoading.value = true
try {
currentProfit.value = await ProfitApi.getProjectProfit(row.projectId) currentProfit.value = await ProfitApi.getProjectProfit(row.projectId)
} finally {
detailLoading.value = false
}
} }
const refreshCurrentProfit = async () => { const refreshCurrentProfit = async () => {
if (!currentProfit.value?.projectId) { if (!currentProfit.value?.projectId) {
return return
} }
detailLoading.value = true
try {
currentProfit.value = await ProfitApi.getProjectProfit(currentProfit.value.projectId) currentProfit.value = await ProfitApi.getProjectProfit(currentProfit.value.projectId)
} finally {
detailLoading.value = false
}
await getList() await getList()
} }
@@ -440,11 +456,18 @@ const profitLossClass = (value?: number) => {
const isUsingContractAmount = (row?: ProfitApi.ProjectProfitVO) => const isUsingContractAmount = (row?: ProfitApi.ProjectProfitVO) =>
!!row && Number(row.finalSettlementAmount || 0) <= 0 && Number(row.contractAmount || 0) > 0 !!row && Number(row.finalSettlementAmount || 0) <= 0 && Number(row.contractAmount || 0) > 0
let activatedOnce = false
onMounted(() => { onMounted(() => {
getList() getList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getList() getList()
}) })
</script> </script>

View File

@@ -526,11 +526,18 @@ const handlePlanningFormSuccess = async () => {
await getPlanningList() await getPlanningList()
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getList() getList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getList() getList()
}) })
</script> </script>

View File

@@ -384,11 +384,18 @@ const submitProjectBudgetExport = async () => {
} }
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getProjectList() getProjectList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求项目与合约规划列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getProjectList() getProjectList()
}) })
</script> </script>

View File

@@ -103,7 +103,9 @@
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<ContentWrap v-if="currentPlanning && formData"> <ContentWrap>
<div v-loading="quarterLoading" class="min-h-320px">
<template v-if="currentPlanning && formData">
<div class="mb-16px flex items-center justify-between gap-16px"> <div class="mb-16px flex items-center justify-between gap-16px">
<div> <div>
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div> <div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
@@ -193,7 +195,7 @@
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-table v-loading="quarterLoading" :data="annualDistributionRows" border> <el-table :data="annualDistributionRows" border>
<el-table-column align="center" label="分配年度" min-width="120" prop="distributionYear" /> <el-table-column align="center" label="分配年度" min-width="120" prop="distributionYear" />
<el-table-column <el-table-column
v-for="quarter in QUARTER_OPTIONS" v-for="quarter in QUARTER_OPTIONS"
@@ -212,10 +214,10 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</ContentWrap> </template>
<ContentWrap v-else> <el-empty v-else-if="!quarterLoading" description="请选择合约规划后查看导出预览" />
<el-empty description="请选择合约规划后查看导出预览" /> </div>
</ContentWrap> </ContentWrap>
</el-col> </el-col>
</el-row> </el-row>
@@ -566,14 +568,12 @@ const loadPlanningRelatedData = async (planning: PlanningApi.ProjectPlanningVO)
} }
quarterLoading.value = true quarterLoading.value = true
try { try {
const [planningDetail, outputSplit, quarterList] = await Promise.all([ const detail = await OutputSplitApi.getProjectOutputSplitPlanningDetail(planning.id)
PlanningApi.getProjectPlanning(planning.id), currentPlanning.value = detail?.planning
OutputSplitApi.getProjectOutputSplitByPlanningId(planning.id), formData.value = detail?.outputSplit
PlanningQuarterApi.getProjectPlanningQuarterListByPlanningId(planning.id) quarterRows.value = detail?.planning
]) ? buildQuarterRows(detail.planning, detail.quarters || [])
currentPlanning.value = planningDetail : []
formData.value = outputSplit
quarterRows.value = buildQuarterRows(planningDetail, quarterList)
} finally { } finally {
quarterLoading.value = false quarterLoading.value = false
} }
@@ -684,11 +684,18 @@ const submitProjectLeadQuarterExport = async () => {
} }
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getProjectList() getProjectList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求项目与合约规划列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getProjectList() getProjectList()
}) })
</script> </script>

View File

@@ -105,7 +105,9 @@
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<ContentWrap v-if="currentPlanning && currentGroup"> <ContentWrap>
<div v-loading="previewLoading" class="min-h-320px">
<template v-if="currentPlanning && currentGroup">
<div class="mb-16px flex items-center justify-between gap-12px"> <div class="mb-16px flex items-center justify-between gap-12px">
<div> <div>
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div> <div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
@@ -252,10 +254,10 @@
</el-table-column> </el-table-column>
</el-table-column> </el-table-column>
</el-table> </el-table>
</ContentWrap> </template>
<ContentWrap v-else> <el-empty v-else-if="!previewLoading" description="请选择合约规划后查看导出预览" />
<el-empty description="请选择合约规划后查看导出预览" /> </div>
</ContentWrap> </ContentWrap>
</el-col> </el-col>
</el-row> </el-row>
@@ -545,11 +547,18 @@ watch(
} }
) )
let activatedOnce = false
onMounted(() => { onMounted(() => {
getProjectList() getProjectList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求项目与合约规划列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getProjectList() getProjectList()
}) })
</script> </script>

View File

@@ -565,7 +565,14 @@ watch(
} }
) )
let activatedOnce = false
onActivated(() => { onActivated(() => {
// immediate watch 已经负责首次预览加载,跳过 KeepAlive 首轮激活避免重复请求。
if (!activatedOnce) {
activatedOnce = true
return
}
refreshCurrentPreview() refreshCurrentPreview()
}) })

View File

@@ -105,7 +105,9 @@
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<ContentWrap v-if="currentPlanning && currentGroup"> <ContentWrap>
<div v-loading="roleLoading" class="min-h-320px">
<template v-if="currentPlanning && currentGroup">
<div class="mb-16px flex items-center justify-between gap-12px"> <div class="mb-16px flex items-center justify-between gap-12px">
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div> <div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
<div class="flex items-center gap-12px"> <div class="flex items-center gap-12px">
@@ -172,10 +174,10 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</ContentWrap> </template>
<ContentWrap v-else> <el-empty v-else-if="!roleLoading" description="请选择合约规划后查看人员分配" />
<el-empty description="请选择合约规划后查看人员分配" /> </div>
</ContentWrap> </ContentWrap>
</el-col> </el-col>
</el-row> </el-row>
@@ -306,7 +308,8 @@
</template> </template>
<template #footer> <template #footer>
<el-button :loading="saveLoading" type="primary" @click="handleSave">保存</el-button> <el-button :loading="saveLoading" @click="handleTemporarySave">临时保存</el-button>
<el-button :loading="saveLoading" type="primary" @click="handleSave">校验保存</el-button>
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
</template> </template>
</Dialog> </Dialog>
@@ -349,6 +352,7 @@ const message = useMessage()
const loading = ref(false) const loading = ref(false)
const planningLoading = ref(false) const planningLoading = ref(false)
const roleLoading = ref(false)
const saveLoading = ref(false) const saveLoading = ref(false)
const total = ref(0) const total = ref(0)
const projectList = ref<ProjectApi.ProjectVO[]>([]) const projectList = ref<ProjectApi.ProjectVO[]>([])
@@ -374,7 +378,7 @@ const queryParams = reactive<ProjectApi.ProjectPageReqVO>({
}) })
const getProjectRowIndex = (index: number) => const getProjectRowIndex = (index: number) =>
(queryParams.pageNo - 1) * queryParams.pageSize + index + 1 (Number(queryParams.pageNo || 1) - 1) * Number(queryParams.pageSize || 10) + index + 1
const queryProjectStartYearValue = computed({ const queryProjectStartYearValue = computed({
get: () => (queryParams.projectStartYear ? String(queryParams.projectStartYear) : undefined), get: () => (queryParams.projectStartYear ? String(queryParams.projectStartYear) : undefined),
@@ -681,7 +685,8 @@ const updateRoleRatio = (row: SpecialtyRoleSplitVO, value?: number) => {
} }
const buildSavePersons = ( const buildSavePersons = (
row: SpecialtyRoleSplitVO row: SpecialtyRoleSplitVO,
validate: boolean
): ):
| { persons: SpecialtyRolePersonVO[]; error?: never } | { persons: SpecialtyRolePersonVO[]; error?: never }
| { persons?: never; error: string } => { | { persons?: never; error: string } => {
@@ -693,35 +698,39 @@ const buildSavePersons = (
if (!hasEmployeeId && !hasRatio) { if (!hasEmployeeId && !hasRatio) {
continue continue
} }
if (!hasEmployeeId || !hasRatio) { if (!validate && !hasEmployeeId) {
continue
}
if (validate && (!hasEmployeeId || !hasRatio)) {
return { error: `${row.roleName} 的员工和比例必须同时填写` } return { error: `${row.roleName} 的员工和比例必须同时填写` }
} }
persons.push({ persons.push({
employeeId: person.employeeId, employeeId: person.employeeId,
employeeName: employeeName || undefined, employeeName: employeeName || undefined,
personRatio: roundRatio(person.personRatio) personRatio: roundRatio(hasRatio ? person.personRatio : 0)
}) })
} }
return { persons } return { persons }
} }
const validateAndBuildSaveItems = () => { const buildSaveItems = (validate: boolean) => {
const items: SpecialtyRoleSplitApi.SpecialtyRoleSplitSaveItemVO[] = [] const items: SpecialtyRoleSplitApi.SpecialtyRoleSplitSaveItemVO[] = []
for (const group of editGroups.value) { for (const group of editGroups.value) {
let roleTotal = 0 let roleTotal = 0
for (const row of group.rows) { for (const row of group.rows) {
const result = buildSavePersons(row) const result = buildSavePersons(row, validate)
if (result.error) { if (result.error) {
message.warning(result.error) message.warning(result.error)
return undefined return undefined
} }
const persons = result.persons || [] const persons = result.persons || []
const personTotalRatio = sumPersonRatios(persons) const personTotalRatio = sumPersonRatios(persons)
if (personTotalRatio > 1 + EPSILON) { if (validate && personTotalRatio > 1 + EPSILON) {
message.warning(`${group.specialtyName}-${row.roleName} 的人员比例合计不能大于 100%`) message.warning(`${group.specialtyName}-${row.roleName} 的人员比例合计不能大于 100%`)
return undefined return undefined
} }
if ( if (
validate &&
row.roleCode === DESIGN_ROLE_CODE && row.roleCode === DESIGN_ROLE_CODE &&
Number(row.roleAmount || 0) > EPSILON && Number(row.roleAmount || 0) > EPSILON &&
persons.length === 0 persons.length === 0
@@ -737,7 +746,7 @@ const validateAndBuildSaveItems = () => {
persons: isProjectLeadRow(row) ? [] : persons persons: isProjectLeadRow(row) ? [] : persons
}) })
} }
if (Math.abs(roleTotal - 1) > EPSILON) { if (validate && Math.abs(roleTotal - 1) > EPSILON) {
message.warning(`${group.specialtyName} 的角色比例合计必须等于 100%`) message.warning(`${group.specialtyName} 的角色比例合计必须等于 100%`)
return undefined return undefined
} }
@@ -798,10 +807,15 @@ const getPlanningList = async () => {
} }
const loadRoleList = async (planningId: number) => { const loadRoleList = async (planningId: number) => {
roleLoading.value = true
try {
const data = await SpecialtyRoleSplitApi.getSpecialtyRoleSplitListByPlanningId(planningId) const data = await SpecialtyRoleSplitApi.getSpecialtyRoleSplitListByPlanningId(planningId)
roleList.value = cloneRoleRows(data) roleList.value = cloneRoleRows(data)
roleList.value.forEach((row) => syncRoleRatiosForGroup(roleList.value, row.specialtyCode)) roleList.value.forEach((row) => syncRoleRatiosForGroup(roleList.value, row.specialtyCode))
syncAllDerivedValues(roleList.value) syncAllDerivedValues(roleList.value)
} finally {
roleLoading.value = false
}
} }
const handleQuery = () => { const handleQuery = () => {
@@ -841,11 +855,11 @@ const openEditDialog = () => {
dialogVisible.value = true dialogVisible.value = true
} }
const handleSave = async () => { const saveRoleSplit = async (temporarySave: boolean) => {
if (!currentPlanning.value?.id) { if (!currentPlanning.value?.id) {
return return
} }
const items = validateAndBuildSaveItems() const items = buildSaveItems(!temporarySave)
if (!items) { if (!items) {
return return
} }
@@ -853,21 +867,35 @@ const handleSave = async () => {
try { try {
await SpecialtyRoleSplitApi.saveSpecialtyRoleSplitBatch({ await SpecialtyRoleSplitApi.saveSpecialtyRoleSplitBatch({
planningId: currentPlanning.value.id, planningId: currentPlanning.value.id,
items items,
temporarySave
}) })
message.success('保存成功') message.success(temporarySave ? '临时保存成功' : '保存成功')
if (!temporarySave) {
dialogVisible.value = false dialogVisible.value = false
}
await loadRoleList(currentPlanning.value.id) await loadRoleList(currentPlanning.value.id)
} finally { } finally {
saveLoading.value = false saveLoading.value = false
} }
} }
const handleTemporarySave = () => saveRoleSplit(true)
const handleSave = () => saveRoleSplit(false)
let activatedOnce = false
onMounted(() => { onMounted(() => {
getProjectList() getProjectList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求项目与合约规划列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getProjectList() getProjectList()
}) })
</script> </script>

View File

@@ -299,11 +299,18 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
let activatedOnce = false
onMounted(() => { onMounted(() => {
getList() getList()
}) })
onActivated(() => { onActivated(() => {
// KeepAlive 首次挂载也会触发 onActivated跳过首轮避免重复请求列表。
if (!activatedOnce) {
activatedOnce = true
return
}
getList() getList()
}) })
</script> </script>