feat(产品需求): 实现产品需求相关代码。
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, 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 addingTopModule = ref(false);
|
||||
const newModuleName = ref('');
|
||||
|
||||
const addingChildParentId = ref<string | undefined>(undefined);
|
||||
const newChildModuleName = ref('');
|
||||
|
||||
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 startAddTopModule() {
|
||||
if (addingTopModule.value || addingChildParentId.value) return;
|
||||
|
||||
addingTopModule.value = true;
|
||||
newModuleName.value = '';
|
||||
|
||||
nextTick(() => {
|
||||
const input = document.querySelector('.new-module-input input') as HTMLInputElement;
|
||||
input?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAddTopModuleConfirm() {
|
||||
const name = newModuleName.value.trim();
|
||||
|
||||
if (!name) {
|
||||
addingTopModule.value = false;
|
||||
newModuleName.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentObjectId.value || !rootModule.value?.id) {
|
||||
addingTopModule.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await fetchCreateRequirementModule({
|
||||
id: undefined,
|
||||
productId: currentObjectId.value,
|
||||
parentId: rootModule.value.id,
|
||||
moduleName: name,
|
||||
remark: null,
|
||||
icon: null,
|
||||
sort: 0
|
||||
});
|
||||
|
||||
if (error) {
|
||||
addingTopModule.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('模块新增成功');
|
||||
addingTopModule.value = false;
|
||||
newModuleName.value = '';
|
||||
await loadModuleTree();
|
||||
emit('refresh');
|
||||
}
|
||||
|
||||
function handleAddTopModuleCancel() {
|
||||
addingTopModule.value = false;
|
||||
newModuleName.value = '';
|
||||
}
|
||||
|
||||
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 (addingTopModule.value || 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>
|
||||
<ElSpace>
|
||||
<ElButton
|
||||
v-auth="{ code: 'project:product:create', source: 'object' }"
|
||||
circle
|
||||
text
|
||||
size="small"
|
||||
@click="startAddTopModule"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-ic-round-plus class="text-16px" />
|
||||
</template>
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</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 v-if="addingTopModule" class="module-tree-item module-tree-item--new">
|
||||
<div class="module-tree-item__icon">
|
||||
<icon-mdi-folder-plus-outline class="text-16px" />
|
||||
</div>
|
||||
<div class="module-tree-item__content">
|
||||
<ElInput
|
||||
v-model="newModuleName"
|
||||
size="small"
|
||||
class="new-module-input module-tree-item__input"
|
||||
placeholder="请输入模块名"
|
||||
@blur="handleAddTopModuleConfirm"
|
||||
@keyup.enter="handleAddTopModuleConfirm"
|
||||
@keyup.esc="handleAddTopModuleCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
Reference in New Issue
Block a user