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

381 lines
8.6 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, nextTick, provide, ref, watch } from 'vue';
import { ElMessageBox } from 'element-plus';
import {
fetchCreateRequirementModule,
fetchDeleteRequirementModule,
fetchGetRequirementModuleTree,
fetchUpdateRequirementModule
} from '@/service/api';
import { useCurrentProduct } from '../../shared/use-current-product';
import ModuleTreeNode from './module-tree-node.vue';
defineOptions({ name: 'RequirementModuleTree' });
interface Props {
requirementTree?: Api.Product.Requirement[];
}
const props = withDefaults(defineProps<Props>(), {
requirementTree: () => []
});
interface Emits {
(e: 'select', moduleId: string | undefined): void;
(e: 'refresh'): void;
}
const emit = defineEmits<Emits>();
const { currentObjectId } = useCurrentProduct();
const loading = ref(false);
const moduleTree = ref<Api.Product.RequirementModule[]>([]);
const selectedModuleId = ref<string | undefined>(undefined);
const rootModule = computed<Api.Product.RequirementModule | null>(() => {
if (moduleTree.value.length === 0) return null;
return moduleTree.value[0];
});
const editingNodeId = ref<string | undefined>(undefined);
const editingName = ref('');
const addingChildParentId = ref<string | undefined>(undefined);
const newChildModuleName = ref('');
const collapsedModuleIds = ref(new Set<string>());
function handleToggleCollapse(moduleId: string) {
const set = collapsedModuleIds.value;
if (set.has(moduleId)) {
set.delete(moduleId);
} else {
set.add(moduleId);
}
collapsedModuleIds.value = new Set(set);
}
provide('collapsedModuleIds', collapsedModuleIds);
provide('toggleCollapse', handleToggleCollapse);
const moduleRequirementCountMap = computed(() => {
const countMap = new Map<string, number>();
function countRequirementsByModule(nodes: Api.Product.Requirement[]): void {
for (const node of nodes) {
const currentCount = countMap.get(node.moduleId) || 0;
countMap.set(node.moduleId, currentCount + 1);
if (node.children?.length) {
countRequirementsByModule(node.children);
}
}
}
if (props.requirementTree?.length) {
countRequirementsByModule(props.requirementTree);
}
return countMap;
});
async function loadModuleTree() {
if (!currentObjectId.value) {
moduleTree.value = [];
return;
}
loading.value = true;
const { error, data } = await fetchGetRequirementModuleTree(currentObjectId.value);
loading.value = false;
if (error || !data) {
moduleTree.value = [];
return;
}
moduleTree.value = data;
if (data.length > 0 && !selectedModuleId.value) {
selectedModuleId.value = data[0].id;
emit('select', data[0].id);
}
}
function handleNodeSelect(moduleId: string) {
if (editingNodeId.value || addingChildParentId.value) return;
selectedModuleId.value = moduleId;
emit('select', moduleId);
}
function handleStartEdit(module: Api.Product.RequirementModule) {
editingNodeId.value = module.id;
editingName.value = module.moduleName;
nextTick(() => {
const input = document.querySelector('.module-tree-item.is-editing .el-input__inner') as HTMLInputElement;
input?.focus();
input?.select();
});
}
async function handleEditConfirm(module: Api.Product.RequirementModule) {
const name = editingName.value.trim();
editingNodeId.value = undefined;
if (!name || name === module.moduleName) {
return;
}
await handleUpdateModuleName(module, name);
}
function handleEditCancel() {
editingNodeId.value = undefined;
}
async function handleUpdateModuleName(module: Api.Product.RequirementModule, name: string) {
if (!currentObjectId.value) return;
const { error } = await fetchUpdateRequirementModule({
id: module.id,
productId: currentObjectId.value,
parentId: module.parentId,
moduleName: name,
remark: module.remark,
icon: module.icon,
sort: module.sort
});
if (error) return;
window.$message?.success('模块名称更新成功');
await loadModuleTree();
emit('refresh');
}
function handleStartAddChild(module: Api.Product.RequirementModule) {
if (addingChildParentId.value) return;
addingChildParentId.value = module.id;
newChildModuleName.value = '';
nextTick(() => {
const input = document.querySelector('.new-child-module-input input') as HTMLInputElement;
input?.focus();
});
}
async function handleAddChildConfirm() {
const name = newChildModuleName.value.trim();
const parentId = addingChildParentId.value;
addingChildParentId.value = undefined;
newChildModuleName.value = '';
if (!name) {
return;
}
if (!currentObjectId.value) {
return;
}
if (!parentId) {
return;
}
const { error } = await fetchCreateRequirementModule({
id: undefined,
productId: currentObjectId.value,
parentId,
moduleName: name,
remark: null,
icon: null,
sort: 0
});
if (error) {
return;
}
window.$message?.success('子模块新增成功');
await loadModuleTree();
emit('refresh');
}
function handleAddChildCancel() {
addingChildParentId.value = undefined;
newChildModuleName.value = '';
}
async function handleDeleteModule(module: Api.Product.RequirementModule) {
if (!currentObjectId.value) return;
try {
await ElMessageBox.confirm(
`确定要删除模块 "${module.moduleName}" 吗?该模块下的所有需求将被一并删除。`,
'删除确认',
{
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning'
}
);
} catch {
return;
}
const { error } = await fetchDeleteRequirementModule({
id: module.id,
productId: currentObjectId.value
});
if (error) return;
window.$message?.success('模块删除成功');
if (selectedModuleId.value === module.id) {
const rootId = rootModule.value?.id || '';
selectedModuleId.value = rootId;
emit('select', rootId);
}
await loadModuleTree();
emit('refresh');
}
watch(
() => currentObjectId.value,
async id => {
if (id) {
selectedModuleId.value = '';
await loadModuleTree();
} else {
moduleTree.value = [];
}
},
{ immediate: true }
);
defineExpose({
loadModuleTree,
selectedModuleId
});
</script>
<template>
<div class="requirement-module-tree-wrapper">
<div class="module-tree-header">
<span class="module-tree-header__title">模块</span>
</div>
<div class="module-tree-list">
<template v-for="data in moduleTree" :key="data.id">
<ModuleTreeNode
:module="data"
:level="0"
:selected-module-id="selectedModuleId"
:editing-node-id="editingNodeId"
:editing-name="editingName"
:adding-child-parent-id="addingChildParentId"
:new-child-module-name="newChildModuleName"
:root-module-id="rootModule?.id"
:module-requirement-count-map="moduleRequirementCountMap"
@select="handleNodeSelect"
@edit="handleStartEdit"
@edit-confirm="handleEditConfirm"
@edit-cancel="handleEditCancel"
@delete="handleDeleteModule"
@add-child="handleStartAddChild"
@add-child-confirm="handleAddChildConfirm"
@add-child-cancel="handleAddChildCancel"
@update-editing-name="editingName = $event"
@update-new-child-module-name="newChildModuleName = $event"
/>
</template>
</div>
</div>
</template>
<style scoped>
.requirement-module-tree-wrapper {
display: flex;
flex-direction: column;
gap: 14px;
}
.module-tree-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.module-tree-header__title {
color: rgb(15 23 42 / 94%);
font-size: 15px;
font-weight: 700;
}
.module-tree-list {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
}
.module-tree-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
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--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__input {
width: 100%;
}
.module-tree-item__input :deep(.el-input__inner) {
height: 28px;
}
</style>