feat(产品需求): 实现产品需求相关代码。
This commit is contained in:
313
src/views/product/requirement/modules/module-tree-node.vue
Normal file
313
src/views/product/requirement/modules/module-tree-node.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user