专业间人员分配添加同步按钮
This commit is contained in:
194
src/views/tjt/shared/SplitPane.vue
Normal file
194
src/views/tjt/shared/SplitPane.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div ref="paneRef" class="tjt-split-pane" :class="{ 'is-resizing': isResizing }">
|
||||
<div class="tjt-split-pane__panel tjt-split-pane__panel--left" :style="{ flexBasis: leftBasis }">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-label="Resize panels"
|
||||
class="tjt-split-pane__resize"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
@keydown="handleKeydown"
|
||||
@pointerdown="startResize"
|
||||
>
|
||||
<span class="tjt-split-pane__resize-handle"></span>
|
||||
</div>
|
||||
|
||||
<div class="tjt-split-pane__panel tjt-split-pane__panel--right">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'TjtSplitPane' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
defaultPercent?: number
|
||||
minPanelWidth?: number
|
||||
}>(),
|
||||
{
|
||||
defaultPercent: 50,
|
||||
minPanelWidth: 320
|
||||
}
|
||||
)
|
||||
|
||||
const RESIZER_WIDTH = 16
|
||||
const KEYBOARD_STEP = 2
|
||||
|
||||
const paneRef = ref<HTMLElement>()
|
||||
const leftPercent = ref(props.defaultPercent)
|
||||
const isResizing = ref(false)
|
||||
|
||||
const leftBasis = computed(() => `calc(${leftPercent.value}% - ${RESIZER_WIDTH / 2}px)`)
|
||||
|
||||
const getBounds = () => {
|
||||
const rect = paneRef.value?.getBoundingClientRect()
|
||||
if (!rect?.width) {
|
||||
return
|
||||
}
|
||||
const minPercent = Math.min(
|
||||
45,
|
||||
((props.minPanelWidth + RESIZER_WIDTH / 2) / rect.width) * 100
|
||||
)
|
||||
return {
|
||||
rect,
|
||||
minPercent,
|
||||
maxPercent: 100 - minPercent
|
||||
}
|
||||
}
|
||||
|
||||
const setLeftPercent = (percent: number) => {
|
||||
const bounds = getBounds()
|
||||
if (!bounds) {
|
||||
leftPercent.value = percent
|
||||
return
|
||||
}
|
||||
leftPercent.value = Math.min(bounds.maxPercent, Math.max(bounds.minPercent, percent))
|
||||
}
|
||||
|
||||
const updateByPointer = (event: PointerEvent) => {
|
||||
const bounds = getBounds()
|
||||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
setLeftPercent(((event.clientX - bounds.rect.left) / bounds.rect.width) * 100)
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
if (!isResizing.value) {
|
||||
return
|
||||
}
|
||||
isResizing.value = false
|
||||
window.removeEventListener('pointermove', updateByPointer)
|
||||
window.removeEventListener('pointerup', stopResize)
|
||||
window.removeEventListener('pointercancel', stopResize)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
|
||||
const startResize = (event: PointerEvent) => {
|
||||
isResizing.value = true
|
||||
event.preventDefault()
|
||||
updateByPointer(event)
|
||||
document.body.style.cursor = 'col-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
window.addEventListener('pointermove', updateByPointer)
|
||||
window.addEventListener('pointerup', stopResize)
|
||||
window.addEventListener('pointercancel', stopResize)
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
setLeftPercent(leftPercent.value - KEYBOARD_STEP)
|
||||
}
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
setLeftPercent(leftPercent.value + KEYBOARD_STEP)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopResize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tjt-split-pane {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tjt-split-pane__panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tjt-split-pane__panel--left {
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
.tjt-split-pane__panel--right {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.tjt-split-pane__resize {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: col-resize;
|
||||
outline: none;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tjt-split-pane__resize::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
content: '';
|
||||
background: var(--el-border-color);
|
||||
}
|
||||
|
||||
.tjt-split-pane__resize-handle {
|
||||
position: sticky;
|
||||
top: calc(50vh - 21px);
|
||||
z-index: 1;
|
||||
width: 6px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
background: var(--el-border-color);
|
||||
transition:
|
||||
width 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.tjt-split-pane__resize:hover .tjt-split-pane__resize-handle,
|
||||
.tjt-split-pane__resize:focus-visible .tjt-split-pane__resize-handle,
|
||||
.tjt-split-pane.is-resizing .tjt-split-pane__resize-handle {
|
||||
width: 8px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.tjt-split-pane {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tjt-split-pane__panel--left,
|
||||
.tjt-split-pane__panel--right {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tjt-split-pane__resize {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -110,6 +110,16 @@
|
||||
<div class="mb-16px flex items-center justify-between gap-12px">
|
||||
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
|
||||
<div class="flex items-center gap-12px">
|
||||
<el-button
|
||||
v-hasPermi="['tjt:specialty-role-split:update']"
|
||||
:disabled="copyPlanningOptions.length === 0"
|
||||
plain
|
||||
type="primary"
|
||||
@click="openCopyDialog"
|
||||
>
|
||||
<Icon class="mr-5px" icon="ep:copy-document" />
|
||||
复制人员设置
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['tjt:specialty-role-split:update']"
|
||||
plain
|
||||
@@ -312,6 +322,31 @@
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model="copyDialogVisible" title="复制其他任务包人员设置" width="560">
|
||||
<el-form label-width="96px">
|
||||
<el-form-item label="来源任务包">
|
||||
<el-select
|
||||
v-model="copySourcePlanningId"
|
||||
class="!w-1/1"
|
||||
filterable
|
||||
placeholder="请选择任务包"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in copyPlanningOptions"
|
||||
:key="item.id"
|
||||
:label="buildPlanningOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button :loading="copyLoading" type="primary" @click="handleCopyConfirm">确定复制</el-button>
|
||||
<el-button @click="copyDialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -356,6 +391,7 @@ const loading = ref(false)
|
||||
const planningLoading = ref(false)
|
||||
const roleLoading = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const copyLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const projectList = ref<ProjectApi.ProjectVO[]>([])
|
||||
const planningList = ref<PlanningApi.ProjectPlanningVO[]>([])
|
||||
@@ -366,6 +402,8 @@ const editRoleList = ref<SpecialtyRoleSplitVO[]>([])
|
||||
const selectedGroupCode = ref('')
|
||||
const editGroupCode = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const copyDialogVisible = ref(false)
|
||||
const copySourcePlanningId = ref<number>()
|
||||
const employeeOptions = ref<EmployeeApi.EmployeeSimpleVO[]>([])
|
||||
const employeeLoading = ref(false)
|
||||
const queryFormRef = ref()
|
||||
@@ -547,6 +585,12 @@ const currentEditGroup = computed(
|
||||
() =>
|
||||
editGroups.value.find((item) => item.specialtyCode === editGroupCode.value) || editGroups.value[0]
|
||||
)
|
||||
const copyPlanningOptions = computed<Array<PlanningApi.ProjectPlanningVO & { id: number }>>(() =>
|
||||
planningList.value.filter(
|
||||
(item): item is PlanningApi.ProjectPlanningVO & { id: number } =>
|
||||
typeof item.id === 'number' && item.id !== currentPlanning.value?.id
|
||||
)
|
||||
)
|
||||
const isProjectLeadRow = (row?: SpecialtyRoleSplitVO) => row?.specialtyCode === PROJECT_LEAD_GROUP_CODE
|
||||
const isEngineeringPrincipalRole = (row?: SpecialtyRoleSplitVO) =>
|
||||
row?.roleCode === ROLE_ENGINEERING_PRINCIPAL
|
||||
@@ -715,9 +759,9 @@ const buildSavePersons = (
|
||||
return { persons }
|
||||
}
|
||||
|
||||
const buildSaveItems = (validate: boolean) => {
|
||||
const buildSaveItems = (validate: boolean, sourceGroups = editGroups.value) => {
|
||||
const items: SpecialtyRoleSplitApi.SpecialtyRoleSplitSaveItemVO[] = []
|
||||
for (const group of editGroups.value) {
|
||||
for (const group of sourceGroups) {
|
||||
let roleTotal = 0
|
||||
for (const row of group.rows) {
|
||||
const result = buildSavePersons(row, validate)
|
||||
@@ -857,6 +901,83 @@ const openEditDialog = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const buildPlanningOptionLabel = (item: PlanningApi.ProjectPlanningVO) => {
|
||||
const parts = [item.planningContent]
|
||||
if (item.planningStartYear) {
|
||||
parts.push(String(item.planningStartYear))
|
||||
}
|
||||
return parts.filter(Boolean).join(' / ')
|
||||
}
|
||||
|
||||
const openCopyDialog = () => {
|
||||
if (!currentPlanning.value?.id) {
|
||||
return
|
||||
}
|
||||
if (!copyPlanningOptions.value.length) {
|
||||
message.warning('暂无可复制的其他任务包')
|
||||
return
|
||||
}
|
||||
copySourcePlanningId.value = copyPlanningOptions.value[0]?.id
|
||||
copyDialogVisible.value = true
|
||||
}
|
||||
|
||||
const buildCopiedRoleRows = (sourceRows: SpecialtyRoleSplitVO[]) => {
|
||||
const sourceMap = new Map(
|
||||
sourceRows.map((item) => [`${item.specialtyCode}:${item.roleCode}`, item] as const)
|
||||
)
|
||||
const rows = roleList.value.map((targetRow) => {
|
||||
const sourceRow = sourceMap.get(`${targetRow.specialtyCode}:${targetRow.roleCode}`)
|
||||
return buildEditableRow({
|
||||
...targetRow,
|
||||
roleRatio: sourceRow?.roleRatio ?? targetRow.roleRatio,
|
||||
persons: isProjectLeadRow(targetRow) ? targetRow.persons || [] : sourceRow?.persons || []
|
||||
})
|
||||
})
|
||||
rows.forEach((row) => syncRoleRatiosForGroup(rows, row.specialtyCode))
|
||||
syncAllDerivedValues(rows)
|
||||
return rows
|
||||
}
|
||||
|
||||
const handleCopyConfirm = async () => {
|
||||
if (!currentPlanning.value?.id || !copySourcePlanningId.value) {
|
||||
message.warning('请选择来源任务包')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await message.confirm('确认用所选任务包的人员分配设置替换当前任务包吗?')
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
copyLoading.value = true
|
||||
try {
|
||||
const sourceData = await SpecialtyRoleSplitApi.getSpecialtyRoleSplitListByPlanningId(
|
||||
copySourcePlanningId.value
|
||||
)
|
||||
const sourceRows = cloneRoleRows(sourceData)
|
||||
if (!sourceRows.length) {
|
||||
message.warning('来源任务包暂无人员分配设置')
|
||||
return
|
||||
}
|
||||
sourceRows.forEach((row) => syncRoleRatiosForGroup(sourceRows, row.specialtyCode))
|
||||
syncAllDerivedValues(sourceRows)
|
||||
const copiedRows = buildCopiedRoleRows(sourceRows)
|
||||
const items = buildSaveItems(false, buildGroups(copiedRows))
|
||||
if (!items) {
|
||||
return
|
||||
}
|
||||
await SpecialtyRoleSplitApi.saveSpecialtyRoleSplitBatch({
|
||||
planningId: currentPlanning.value.id,
|
||||
items,
|
||||
temporarySave: true
|
||||
})
|
||||
message.success('复制人员分配设置成功')
|
||||
copyDialogVisible.value = false
|
||||
await loadRoleList(currentPlanning.value.id)
|
||||
} finally {
|
||||
copyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveRoleSplit = async (temporarySave: boolean) => {
|
||||
if (!currentPlanning.value?.id) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user