Files
cn-rdms-web/src/views/product/requirement/modules/module-tree-node.vue

314 lines
8.2 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed } from 'vue';
defineOptions({ name: 'ModuleTreeNode' });
interface Props {
module: Api.Product.RequirementModule;
level?: number;
selectedModuleId?: string;
editingNodeId?: string | undefined;
editingName?: string;
addingChildParentId?: string | undefined;
newChildModuleName?: string;
rootModuleId?: string;
moduleRequirementCountMap?: Map<string, number>;
}
const props = withDefaults(defineProps<Props>(), {
level: 0
});
const emit = defineEmits([
'select',
'edit',
'editConfirm',
'editCancel',
'delete',
'addChild',
'addChildConfirm',
'addChildCancel',
'updateEditingName',
'updateNewChildModuleName'
]);
const isRootModule = computed(() => props.module.id === props.rootModuleId);
const isSelected = computed(() => props.selectedModuleId === props.module.id);
const isEditing = computed(() => props.editingNodeId === props.module.id);
const isAddingChild = computed(() => props.addingChildParentId === props.module.id);
const hasChildren = computed(() => props.module.children && props.module.children.length > 0);
const hasRequirements = computed(() => {
const moduleId = props.module.id;
if (!moduleId || !props.moduleRequirementCountMap) return false;
return (props.moduleRequirementCountMap.get(moduleId) || 0) > 0;
});
const canDeleteModule = computed(() => !hasChildren.value && !hasRequirements.value);
const indentStyle = computed(() => {
if (props.level === 0) return {};
const indent = 24 + (props.level - 1) * 24;
return {
width: `calc(100% - ${indent}px)`,
marginLeft: `${indent}px`
};
});
function handleClick() {
if (props.editingNodeId || props.addingChildParentId) return;
emit('select', props.module.id);
}
function handleStartEdit() {
emit('edit', props.module);
}
function handleEditConfirm() {
emit('editConfirm', props.module);
}
function handleEditCancel() {
emit('editCancel');
}
function handleStartAddChild() {
emit('addChild', props.module);
}
function handleDelete() {
emit('delete', props.module);
}
function handleAddChildConfirm() {
emit('addChildConfirm');
}
function handleAddChildCancel() {
emit('addChildCancel');
}
</script>
<template>
<div class="module-tree-node">
<div
class="module-tree-item"
:class="{
'is-root': isRootModule,
'is-active': isSelected,
'is-editing': isEditing
}"
:style="indentStyle"
@click="handleClick"
>
<div class="module-tree-item__icon">
<icon-mdi-folder-open v-if="isRootModule" class="text-16px" />
<icon-mdi-folder-outline v-else class="text-16px" />
</div>
<div class="module-tree-item__content">
<span v-if="!isEditing" class="module-tree-item__label">{{ module.moduleName }}</span>
<ElInput
v-else
:model-value="editingName"
size="small"
class="module-tree-item__input"
placeholder="请输入模块名"
@update:model-value="emit('updateEditingName', $event)"
@blur="handleEditConfirm"
@keyup.enter="handleEditConfirm"
@keyup.esc="handleEditCancel"
/>
</div>
<div v-if="!isRootModule && !isEditing" class="module-tree-item__actions">
<ElDropdown trigger="click">
<ElButton text size="small" class="module-tree-item__more-btn">
<icon-mdi-dots-horizontal class="text-14px" />
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
v-auth="{ code: 'project:product:create', source: 'object' }"
@click="handleStartAddChild"
>
<div class="flex items-center gap-6px">
<icon-ic-round-plus class="text-14px" />
<span>新增子模块</span>
</div>
</ElDropdownItem>
<ElDropdownItem v-auth="{ code: 'project:product:update', source: 'object' }" @click="handleStartEdit">
<div class="flex items-center gap-6px">
<icon-mdi-pencil-outline class="text-14px" />
<span>编辑</span>
</div>
</ElDropdownItem>
<ElDropdownItem
v-if="canDeleteModule"
v-auth="{ code: 'project:product:delete', source: 'object' }"
divided
@click="handleDelete"
>
<div class="flex items-center gap-6px text-error">
<icon-mdi-delete-outline class="text-14px" />
<span>删除</span>
</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</div>
<template v-if="hasChildren">
<ModuleTreeNode
v-for="child in module.children"
:key="child.id"
:module="child"
:level="level + 1"
:selected-module-id="selectedModuleId"
:editing-node-id="editingNodeId"
:editing-name="editingName"
:adding-child-parent-id="addingChildParentId"
:new-child-module-name="newChildModuleName"
:root-module-id="rootModuleId"
:module-requirement-count-map="moduleRequirementCountMap"
@select="emit('select', $event)"
@edit="emit('edit', $event)"
@edit-confirm="emit('editConfirm', $event)"
@edit-cancel="emit('editCancel')"
@delete="emit('delete', $event)"
@add-child="emit('addChild', $event)"
@add-child-confirm="emit('addChildConfirm')"
@add-child-cancel="emit('addChildCancel')"
@update-editing-name="emit('updateEditingName', $event)"
@update-new-child-module-name="emit('updateNewChildModuleName', $event)"
/>
</template>
<div
v-if="isAddingChild"
class="module-tree-item module-tree-item--new"
:style="{
width: indentStyle.width,
marginLeft: level === 0 ? '24px' : `calc(24px + ${level * 24}px)`
}"
>
<div class="module-tree-item__icon">
<icon-mdi-folder-plus-outline class="text-16px" />
</div>
<div class="module-tree-item__content">
<ElInput
:model-value="newChildModuleName"
size="small"
class="new-child-module-input module-tree-item__input"
placeholder="请输入模块名"
@update:model-value="emit('updateNewChildModuleName', $event)"
@blur="handleAddChildConfirm"
@keyup.enter="handleAddChildConfirm"
@keyup.esc="handleAddChildCancel"
/>
</div>
</div>
</div>
</template>
<style scoped>
.module-tree-node {
display: flex;
flex-direction: column;
gap: 10px;
}
.module-tree-item {
display: flex;
align-items: center;
gap: 10px;
min-height: 42px;
padding: 0 14px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 14px;
background-color: rgb(248 250 252 / 96%);
color: rgb(71 85 105 / 94%);
font-size: 14px;
cursor: pointer;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
color 0.2s ease,
transform 0.2s ease;
}
.module-tree-item:hover {
transform: translateY(-1px);
border-color: rgb(148 163 184 / 56%);
}
.module-tree-item.is-active {
border-color: rgb(13 148 136 / 42%);
background-color: rgb(240 253 250 / 98%);
color: rgb(15 118 110 / 96%);
font-weight: 600;
}
.module-tree-item.is-root:not(.is-active) .module-tree-item__icon {
color: rgb(13 148 136 / 80%);
}
.module-tree-item--new {
border-style: dashed;
border-color: rgb(148 163 184 / 56%);
}
.module-tree-item__icon {
display: flex;
align-items: center;
flex-shrink: 0;
color: rgb(100 116 139 / 80%);
}
.module-tree-item__content {
flex: 1;
min-width: 0;
overflow: hidden;
}
.module-tree-item__label {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.module-tree-item__input {
width: 100%;
}
.module-tree-item__input :deep(.el-input__inner) {
height: 28px;
}
.module-tree-item__actions {
display: flex;
align-items: center;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.2s ease;
}
.module-tree-item:hover .module-tree-item__actions {
opacity: 1;
}
.module-tree-item.is-editing .module-tree-item__actions {
opacity: 0;
}
.module-tree-item__more-btn {
padding: 4px;
}
</style>