专业间人员分配添加同步按钮
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="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">
|
||||||
|
<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
|
<el-button
|
||||||
v-hasPermi="['tjt:specialty-role-split:update']"
|
v-hasPermi="['tjt:specialty-role-split:update']"
|
||||||
plain
|
plain
|
||||||
@@ -312,6 +322,31 @@
|
|||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -356,6 +391,7 @@ const loading = ref(false)
|
|||||||
const planningLoading = ref(false)
|
const planningLoading = ref(false)
|
||||||
const roleLoading = ref(false)
|
const roleLoading = ref(false)
|
||||||
const saveLoading = ref(false)
|
const saveLoading = ref(false)
|
||||||
|
const copyLoading = ref(false)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const projectList = ref<ProjectApi.ProjectVO[]>([])
|
const projectList = ref<ProjectApi.ProjectVO[]>([])
|
||||||
const planningList = ref<PlanningApi.ProjectPlanningVO[]>([])
|
const planningList = ref<PlanningApi.ProjectPlanningVO[]>([])
|
||||||
@@ -366,6 +402,8 @@ const editRoleList = ref<SpecialtyRoleSplitVO[]>([])
|
|||||||
const selectedGroupCode = ref('')
|
const selectedGroupCode = ref('')
|
||||||
const editGroupCode = ref('')
|
const editGroupCode = ref('')
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
|
const copyDialogVisible = ref(false)
|
||||||
|
const copySourcePlanningId = ref<number>()
|
||||||
const employeeOptions = ref<EmployeeApi.EmployeeSimpleVO[]>([])
|
const employeeOptions = ref<EmployeeApi.EmployeeSimpleVO[]>([])
|
||||||
const employeeLoading = ref(false)
|
const employeeLoading = ref(false)
|
||||||
const queryFormRef = ref()
|
const queryFormRef = ref()
|
||||||
@@ -547,6 +585,12 @@ const currentEditGroup = computed(
|
|||||||
() =>
|
() =>
|
||||||
editGroups.value.find((item) => item.specialtyCode === editGroupCode.value) || editGroups.value[0]
|
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 isProjectLeadRow = (row?: SpecialtyRoleSplitVO) => row?.specialtyCode === PROJECT_LEAD_GROUP_CODE
|
||||||
const isEngineeringPrincipalRole = (row?: SpecialtyRoleSplitVO) =>
|
const isEngineeringPrincipalRole = (row?: SpecialtyRoleSplitVO) =>
|
||||||
row?.roleCode === ROLE_ENGINEERING_PRINCIPAL
|
row?.roleCode === ROLE_ENGINEERING_PRINCIPAL
|
||||||
@@ -715,9 +759,9 @@ const buildSavePersons = (
|
|||||||
return { persons }
|
return { persons }
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildSaveItems = (validate: boolean) => {
|
const buildSaveItems = (validate: boolean, sourceGroups = editGroups.value) => {
|
||||||
const items: SpecialtyRoleSplitApi.SpecialtyRoleSplitSaveItemVO[] = []
|
const items: SpecialtyRoleSplitApi.SpecialtyRoleSplitSaveItemVO[] = []
|
||||||
for (const group of editGroups.value) {
|
for (const group of sourceGroups) {
|
||||||
let roleTotal = 0
|
let roleTotal = 0
|
||||||
for (const row of group.rows) {
|
for (const row of group.rows) {
|
||||||
const result = buildSavePersons(row, validate)
|
const result = buildSavePersons(row, validate)
|
||||||
@@ -857,6 +901,83 @@ const openEditDialog = () => {
|
|||||||
dialogVisible.value = true
|
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) => {
|
const saveRoleSplit = async (temporarySave: boolean) => {
|
||||||
if (!currentPlanning.value?.id) {
|
if (!currentPlanning.value?.id) {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user