fix(产品需求): 修复产品需求在测试后存在的问题。

This commit is contained in:
dk
2026-05-09 13:42:04 +08:00
parent f4f43814b3
commit f0ea903d59
13 changed files with 706 additions and 384 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, inject, ref, type Ref } from 'vue';
defineOptions({ name: 'ModuleTreeNode' });
@@ -32,15 +32,15 @@ const emit = defineEmits([
'updateNewChildModuleName'
]);
const collapsedModuleIds = inject<Ref<Set<string>>>('collapsedModuleIds', ref(new Set()));
const toggleCollapse = inject<(id: string) => void>('toggleCollapse', () => {});
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 isCollapsed = computed(() => hasChildren.value && props.module.id ? collapsedModuleIds.value.has(props.module.id) : false);
const hasRequirements = computed(() => {
const moduleId = props.module.id;
@@ -91,6 +91,12 @@ function handleAddChildConfirm() {
function handleAddChildCancel() {
emit('addChildCancel');
}
function handleToggle() {
if (props.module.id) {
toggleCollapse(props.module.id);
}
}
</script>
<template>
@@ -105,6 +111,9 @@ function handleAddChildCancel() {
:style="indentStyle"
@click="handleClick"
>
<div class="module-tree-item__toggle" :class="{ 'is-expanded': hasChildren && !isCollapsed }" @click.stop="handleToggle">
<icon-ic-round-chevron-right v-if="hasChildren" class="text-14px" />
</div>
<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" />
@@ -124,7 +133,7 @@ function handleAddChildCancel() {
/>
</div>
<div v-if="!isRootModule && !isEditing" class="module-tree-item__actions">
<div v-if="!isEditing" class="module-tree-item__actions" @click.stop>
<ElDropdown trigger="click">
<ElButton text size="small" class="module-tree-item__more-btn">
<icon-mdi-dots-horizontal class="text-14px" />
@@ -140,14 +149,18 @@ function handleAddChildCancel() {
<span>新增子模块</span>
</div>
</ElDropdownItem>
<ElDropdownItem v-auth="{ code: 'project:product:update', source: 'object' }" @click="handleStartEdit">
<ElDropdownItem
v-if="!isRootModule"
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-if="!isRootModule && canDeleteModule"
v-auth="{ code: 'project:product:delete', source: 'object' }"
divided
@click="handleDelete"
@@ -163,7 +176,7 @@ function handleAddChildCancel() {
</div>
</div>
<template v-if="hasChildren">
<template v-if="hasChildren && !isCollapsed">
<ModuleTreeNode
v-for="child in module.children"
:key="child.id"
@@ -270,6 +283,23 @@ function handleAddChildCancel() {
color: rgb(100 116 139 / 80%);
}
.module-tree-item__toggle {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex-shrink: 0;
cursor: pointer;
user-select: none;
transition: transform 0.2s ease;
color: rgb(148 163 184);
}
.module-tree-item__toggle.is-expanded svg {
transform: rotate(90deg);
}
.module-tree-item__content {
flex: 1;
min-width: 0;

View File

@@ -118,8 +118,7 @@ async function handleSubmit() {
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElFormItem label="需求标题">
<span class="text-14px">{{ requirementTitle }}</span>
<ElFormItem :label="`需求名称:${requirementTitle}`">
</ElFormItem>
<ElFormItem v-if="isClaimAction" label="是否需要评审" prop="reviewChoice">

View File

@@ -45,12 +45,12 @@ interface Model {
title: string;
description: string;
reviewRequired: number;
completionDate: string;
moduleId: string;
category: string;
priority: number | null;
proposerId: string;
currentHandlerUserId: string;
workHours: number | null;
sort: number;
}
@@ -64,11 +64,28 @@ const memberUserOptions = computed(() => {
return props.memberOptions.filter(m => m.status === 0);
});
const moduleTreeProps = {
label: 'moduleName',
value: 'id',
children: 'children'
};
const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() => {
const options: Array<{ label: string; value: string }> = [];
function walk(modules: Api.Product.RequirementModule[], parentPath: string) {
for (const module of modules) {
const currentPath = `${parentPath}/${module.moduleName}`;
options.push({
label: currentPath,
value: module.id || ''
});
if (module.children?.length) {
walk(module.children, currentPath);
}
}
}
if (moduleTree.value.length > 0) {
walk(moduleTree.value, '');
}
return options;
});
const reviewRequiredOptions = [
{ label: '不需要', value: 0 },
@@ -76,12 +93,12 @@ const reviewRequiredOptions = [
];
const rules = {
title: [createRequiredRule('请输入需求标题')],
category: [createRequiredRule('请选择分类')],
title: [createRequiredRule('请输入需求名称')],
category: [createRequiredRule('请选择需求类型')],
priority: [createRequiredRule('请选择优先级')],
proposerId: [createRequiredRule('请选择提出人')],
currentHandlerUserId: [createRequiredRule('请选择负责人')],
completionDate: [createRequiredRule('请选择预期完成时间')]
workHours: [createRequiredRule('请输入所需工时')]
} satisfies Record<string, App.Global.FormRule[]>;
function createDefaultModel(): Model {
@@ -89,12 +106,12 @@ function createDefaultModel(): Model {
title: '',
description: '',
reviewRequired: 0,
completionDate: '',
moduleId: props.defaultModuleId || '0',
category: '功能需求',
priority: 1,
proposerId: '',
currentHandlerUserId: '',
workHours: null,
sort: 0
};
}
@@ -114,6 +131,11 @@ async function handleSubmit() {
return;
}
const proposer = memberUserOptions.value.find(m => m.userId === model.value.proposerId);
const proposerNickname = proposer?.userNickname || '';
const handler = memberUserOptions.value.find(m => m.userId === model.value.currentHandlerUserId);
const currentHandlerUserNickname = handler?.userNickname || '';
const payload: Api.Product.SaveRequirementParams = {
productId: props.productId,
moduleId: model.value.moduleId || '0',
@@ -123,9 +145,11 @@ async function handleSubmit() {
category: model.value.category,
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
proposerId: model.value.proposerId,
proposerNickname,
currentHandlerUserId: model.value.currentHandlerUserId,
currentHandlerUserNickname,
implementProjectId: null,
completionDate: model.value.completionDate,
workHours: model.value.workHours || 0,
sort: model.value.sort
};
@@ -188,32 +212,8 @@ watch(
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="标题" prop="title">
<ElInput v-model="model.title" clearable maxlength="256" placeholder="请输入需求标题" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="预期完成时间" prop="completionDate" style="width: 100%">
<ElDatePicker
v-model="model.completionDate"
type="datetime"
class="w-full"
placeholder="选择预期完成时间"
value-format="x"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="描述">
<ElInput
v-model="model.description"
type="textarea"
:rows="6"
maxlength="2000"
show-word-limit
placeholder="请输入需求描述"
/>
<ElFormItem label="需求名称" prop="title">
<ElInput v-model="model.title" clearable maxlength="256" placeholder="请输入需求名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -228,22 +228,33 @@ watch(
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="模块">
<ElTreeSelect
v-model="model.moduleId"
:data="moduleTree"
:props="moduleTreeProps"
class="w-full"
check-strictly
:render-after-expand="false"
placeholder="请选择所属模块"
<ElCol :span="24">
<ElFormItem label="内容">
<ElInput
v-model="model.description"
type="textarea"
:rows="6"
maxlength="2000"
show-word-limit
placeholder="请输入需求内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="分类" prop="category">
<DictSelect v-model="model.category" :dict-code="categoryDictCode" filterable placeholder="请选择分类" />
<ElFormItem label="模块">
<ElSelect v-model="model.moduleId" class="w-full" filterable placeholder="请选择所属模块">
<ElOption
v-for="item in flatModuleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="需求类型" prop="category">
<DictSelect v-model="model.category" :dict-code="categoryDictCode" filterable placeholder="请选择需求类型" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -281,6 +292,18 @@ watch(
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="所需工时" prop="workHours">
<ElInputNumber
v-model="model.workHours"
class="w-full"
:min="0"
:max="9999"
:precision="1"
placeholder="请输入所需工时"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="排序值">
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />

View File

@@ -1,15 +1,13 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { fetchGetRequirement, fetchGetRequirementModuleTree, fetchUpdateRequirement } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import {computed, nextTick, ref, watch} from 'vue';
import {fetchGetRequirement, fetchGetRequirementModuleTree, fetchUpdateRequirement} from '@/service/api';
import {useForm, useFormRules} from '@/hooks/common/form';
import {useDict} from '@/hooks/business/dict';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import DictSelect from '@/components/custom/dict-select.vue';
import ReadonlyField from '@/components/custom/readonly-field.vue';
import MemberSelectOption from './member-select-option.vue';
defineOptions({ name: 'RequirementDetailDialog' });
defineOptions({name: 'RequirementDetailDialog'});
type DialogMode = 'view' | 'edit';
@@ -34,11 +32,11 @@ const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const {formRef, validate} = useForm();
const {createRequiredRule} = useFormRules();
const { getLabel: getCategoryLabel } = useDict(() => props.categoryDictCode);
const { getLabel: getPriorityLabel, enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
const {getLabel: getCategoryLabel} = useDict(() => props.categoryDictCode);
const {getLabel: getPriorityLabel, enabledDictData: priorityDictData} = useDict(() => props.priorityDictCode);
const priorityOptions = computed(() => {
return priorityDictData.value.map(item => ({
@@ -51,13 +49,15 @@ interface Model {
title: string;
description: string;
reviewRequired: number;
completionDate: string;
moduleId: string;
category: string;
priority: number | null;
proposerId: string;
proposerNickname: string;
currentHandlerUserId: string;
currentHandlerUserNickname: string;
implementProjectId: string | null;
workHours: number | null;
sort: number;
lastStatusReason: string;
}
@@ -87,6 +87,7 @@ const memberLabelMap = computed(() => {
const moduleLabelMap = computed(() => {
const map = new Map<string | undefined, string>();
function traverse(modules: Api.Product.RequirementModule[]) {
for (const module of modules) {
map.set(module.id, module.moduleName);
@@ -95,29 +96,46 @@ const moduleLabelMap = computed(() => {
}
}
}
traverse(moduleTree.value);
return map;
});
const moduleTreeProps = {
label: 'moduleName',
value: 'id',
children: 'children'
};
const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() => {
const options: Array<{ label: string; value: string }> = [];
function walk(modules: Api.Product.RequirementModule[], parentPath: string) {
for (const module of modules) {
const currentPath = `${parentPath}/${module.moduleName}`;
options.push({
label: currentPath,
value: module.id || ''
});
if (module.children?.length) {
walk(module.children, currentPath);
}
}
}
if (moduleTree.value.length > 0) {
walk(moduleTree.value, '');
}
return options;
});
const reviewRequiredOptions = [
{ label: '不需要', value: 0 },
{ label: '需要', value: 1 }
{label: '不需要', value: 0},
{label: '需要', value: 1}
];
const rules = computed(() => {
const baseRules: Record<string, App.Global.FormRule[]> = {
title: isEditMode.value ? [createRequiredRule('请输入需求标题')] : [],
category: isEditMode.value ? [createRequiredRule('请选择分类')] : [],
title: isEditMode.value ? [createRequiredRule('请输入需求名称')] : [],
category: isEditMode.value ? [createRequiredRule('请选择需求类型')] : [],
priority: isEditMode.value ? [createRequiredRule('请选择优先级')] : [],
proposerId: isEditMode.value ? [createRequiredRule('请选择提出人')] : [],
currentHandlerUserId: isEditMode.value ? [createRequiredRule('请选择负责人')] : [],
completionDate: isEditMode.value ? [createRequiredRule('请选择预期完成时间')] : []
currentHandlerUserId: isEditMode.value ? [createRequiredRule('请选择负责人')] : []
};
return baseRules;
@@ -128,13 +146,15 @@ function createDefaultModel(): Model {
title: '',
description: '',
reviewRequired: 0,
completionDate: '',
moduleId: '0',
category: '',
priority: 1,
proposerId: '',
proposerNickname: '',
currentHandlerUserId: '',
currentHandlerUserNickname: '',
implementProjectId: null,
workHours: null,
sort: 0,
lastStatusReason: ''
};
@@ -167,13 +187,15 @@ async function handleSubmit() {
category: model.value.category,
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
proposerId: model.value.proposerId,
proposerNickname: model.value.proposerNickname,
currentHandlerUserId: model.value.currentHandlerUserId,
currentHandlerUserNickname: model.value.currentHandlerUserNickname,
implementProjectId: model.value.implementProjectId,
completionDate: model.value.completionDate,
workHours: model.value.workHours || 0,
sort: model.value.sort
};
const { error } = await fetchUpdateRequirement(updatePayload);
const {error} = await fetchUpdateRequirement(updatePayload);
submitting.value = false;
@@ -192,7 +214,7 @@ async function loadModuleTree() {
return;
}
const { error, data } = await fetchGetRequirementModuleTree(props.productId);
const {error, data} = await fetchGetRequirementModuleTree(props.productId);
if (error || !data) {
moduleTree.value = [];
@@ -209,7 +231,7 @@ async function loadRequirementDetail() {
loading.value = true;
const { error, data } = await fetchGetRequirement(props.requirement.id, props.productId);
const {error, data} = await fetchGetRequirement(props.requirement.id, props.productId);
loading.value = false;
@@ -221,13 +243,15 @@ async function loadRequirementDetail() {
title: data.title || '',
description: data.description || '',
reviewRequired: data.reviewRequired ?? 0,
completionDate: data.completionDate || '',
moduleId: data.moduleId || '0',
category: data.category || '',
priority: data.priority ?? null,
proposerId: data.proposerId || '',
proposerNickname: data.proposerNickname || '',
currentHandlerUserId: data.currentHandlerUserId || '',
currentHandlerUserNickname: data.currentHandlerUserNickname || '',
implementProjectId: data.implementProjectId || null,
workHours: data.workHours ?? null,
sort: data.sort ?? 0,
lastStatusReason: data.lastStatusReason || ''
};
@@ -265,33 +289,17 @@ watch(
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="标题" prop="title">
<template v-if="isViewMode">
<ReadonlyField :value="model.title" />
</template>
<ElInput v-else v-model="model.title" clearable maxlength="256" placeholder="请输入需求标题" />
<ElFormItem label="需求名称" prop="title">
<ReadonlyField :value="model.title" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="预期完成时间" prop="completionDate" style="width: 100%">
<template v-if="isViewMode">
<ReadonlyField
:value="model.completionDate ? dayjs(Number(model.completionDate)).format('YYYY-MM-DD HH:mm:ss') : '--'"
/>
</template>
<ElDatePicker
v-else
v-model="model.completionDate"
type="datetime"
class="w-full"
placeholder="选择预期完成时间"
value-format="x"
style="width: 100%"
/>
<ElFormItem label="是否需要评审">
<ReadonlyField :value="reviewRequiredOptions.find(opt => opt.value === model.reviewRequired)?.label"/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="描述">
<ElFormItem label="内容">
<template v-if="isViewMode">
<div class="readonly-textarea">
{{ model.description || '--' }}
@@ -304,44 +312,28 @@ watch(
:rows="6"
maxlength="2000"
show-word-limit
placeholder="请输入需求描述"
placeholder="请输入需求内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="是否需要评审">
<ReadonlyField :value="reviewRequiredOptions.find(opt => opt.value === model.reviewRequired)?.label" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="模块">
<template v-if="isViewMode">
<ReadonlyField :value="moduleLabelMap.get(model.moduleId) || '--'" />
</template>
<ElTreeSelect
v-else
v-model="model.moduleId"
:data="moduleTree"
:props="moduleTreeProps"
class="w-full"
check-strictly
:render-after-expand="false"
placeholder="请选择所属模块"
/>
<ElSelect v-else v-model="model.moduleId" class="w-full" filterable placeholder="请选择所属模块">
<ElOption
v-for="item in flatModuleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="分类" prop="category">
<template v-if="isViewMode">
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
</template>
<DictSelect
v-else
v-model="model.category"
:dict-code="categoryDictCode"
filterable
placeholder="请选择分类"
/>
<ElFormItem label="需求类型" prop="category">
<ReadonlyField :value="getCategoryLabel(model.category) || '--'"/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -352,31 +344,19 @@ watch(
/>
</template>
<ElSelect v-else v-model="model.priority" class="w-full" filterable placeholder="请选择优先级">
<ElOption v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value" />
<ElOption v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value"/>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="提出人" prop="proposerId">
<template v-if="isViewMode">
<ReadonlyField :value="memberLabelMap.get(model.proposerId) || '--'" />
</template>
<ElSelect v-else v-model="model.proposerId" class="w-full" filterable placeholder="请选择提出人">
<ElOption
v-for="item in memberUserOptions"
:key="item.userId"
:label="item.userNickname"
:value="item.userId"
>
<MemberSelectOption :nickname="item.userNickname" :role-name="item.roleName" />
</ElOption>
</ElSelect>
<ReadonlyField :value="memberLabelMap.get(model.proposerId) || '--'"/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="负责人" prop="currentHandlerUserId">
<template v-if="isViewMode">
<ReadonlyField :value="memberLabelMap.get(model.currentHandlerUserId) || '--'" />
<ReadonlyField :value="memberLabelMap.get(model.currentHandlerUserId) || '--'"/>
</template>
<ElSelect v-else v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
<ElOption
@@ -385,22 +365,38 @@ watch(
:label="item.userNickname"
:value="item.userId"
>
<MemberSelectOption :nickname="item.userNickname" :role-name="item.roleName" />
<MemberSelectOption :nickname="item.userNickname" :role-name="item.roleName"/>
</ElOption>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="所需工时">
<template v-if="isViewMode">
<ReadonlyField :value="model.workHours != null ? `${model.workHours}小时` : '--'"/>
</template>
<ElInputNumber
v-else
v-model="model.workHours"
class="w-full"
:min="0"
:max="9999"
:precision="1"
placeholder="请输入所需工时"
/>
</ElFormItem>
</ElCol>
<ElCol v-if="isViewMode" :span="12">
<ElFormItem label="实现项目">
<ReadonlyField :value="model.implementProjectId || '--'" />
<ReadonlyField :value="model.implementProjectId || '--'"/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="排序值">
<template v-if="isViewMode">
<ReadonlyField :value="model.sort" />
<ReadonlyField :value="model.sort"/>
</template>
<ElInputNumber v-else v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
<ElInputNumber v-else v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值"/>
</ElFormItem>
</ElCol>
<ElCol v-if="isViewMode && model.lastStatusReason" :span="24">

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { computed, nextTick, provide, ref, watch } from 'vue';
import { ElMessageBox } from 'element-plus';
import {
fetchCreateRequirementModule,
@@ -41,12 +41,24 @@ const rootModule = computed<Api.Product.RequirementModule | null>(() => {
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 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>();
@@ -98,59 +110,6 @@ function handleNodeSelect(moduleId: string) {
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;
@@ -199,7 +158,7 @@ async function handleUpdateModuleName(module: Api.Product.RequirementModule, nam
}
function handleStartAddChild(module: Api.Product.RequirementModule) {
if (addingTopModule.value || addingChildParentId.value) return;
if (addingChildParentId.value) return;
addingChildParentId.value = module.id;
newChildModuleName.value = '';
@@ -312,19 +271,6 @@ defineExpose({
<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">
@@ -351,23 +297,6 @@ defineExpose({
@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>

View File

@@ -63,18 +63,18 @@ onMounted(async () => {
<template>
<TableSearchPanel :model="model" :action-col-lg="6" @reset="reset" @search="search">
<ElCol :lg="6" :md="12" :sm="12">
<ElFormItem label="标题">
<ElInput v-model="model.title" clearable placeholder="输入需求标题" />
<ElFormItem label="需求名称">
<ElInput v-model="model.title" clearable placeholder="输入需求名称" />
</ElFormItem>
</ElCol>
<ElCol :lg="6" :md="12" :sm="12">
<ElFormItem label="分类">
<ElFormItem label="需求类型">
<DictSelect
v-model="model.category"
:dict-code="categoryDictCode"
clearable
filterable
placeholder="筛选分类"
placeholder="筛选需求类型"
/>
</ElFormItem>
</ElCol>
@@ -111,12 +111,12 @@ onMounted(async () => {
</ElFormItem>
</ElCol>
<ElCol :lg="6" :md="12" :sm="12">
<ElFormItem label="来源类型">
<ElFormItem label="需求来源">
<DictSelect
v-model="model.sourceType"
:dict-code="RDMS_REQ_SOURCE_TYPE_DICT_CODE"
clearable
placeholder="筛选来源类型"
placeholder="筛选需求来源"
/>
</ElFormItem>
</ElCol>

View File

@@ -4,7 +4,6 @@ import { fetchSplitRequirement } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import DictSelect from '@/components/custom/dict-select.vue';
import MemberSelectOption from './member-select-option.vue';
defineOptions({ name: 'RequirementSplitDialog' });
@@ -48,7 +47,7 @@ interface Model {
category: string;
priority: number | null;
currentHandlerUserId: string;
completionDate: string;
workHours: number | null;
sort: number;
}
@@ -67,11 +66,10 @@ const reviewRequiredOptions = [
];
const rules = {
title: [createRequiredRule('请输入子需求标题')],
category: [createRequiredRule('请选择分类')],
title: [createRequiredRule('请输入子需求名称')],
priority: [createRequiredRule('请选择优先级')],
currentHandlerUserId: [createRequiredRule('请选择负责人')],
completionDate: [createRequiredRule('请选择预期完成时间')]
workHours: [createRequiredRule('请输入所需工时')]
} satisfies Record<string, App.Global.FormRule[]>;
function createDefaultModel(): Model {
@@ -82,7 +80,7 @@ function createDefaultModel(): Model {
category: '',
priority: 1,
currentHandlerUserId: '',
completionDate: '',
workHours: null,
sort: 0
};
}
@@ -102,23 +100,26 @@ async function handleSubmit() {
return;
}
const proposerNickname = props.parentRequirement.proposerNickname || '';
const currentHandlerUserNickname = props.parentRequirement.currentHandlerUserNickname || '';
const payload: Api.Product.SplitRequirementParams = {
parentId: props.parentRequirement.id,
productId: props.productId,
moduleId: props.parentRequirement.moduleId,
proposerId: props.parentRequirement.proposerId,
proposerNickname,
currentHandlerUserNickname,
title: model.value.title.trim(),
description: getNullableText(model.value.description),
reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired,
category: model.value.category,
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
currentHandlerUserId: model.value.currentHandlerUserId,
completionDate: model.value.completionDate,
workHours: model.value.workHours || 0,
sort: model.value.sort
};
console.log('payload', payload);
submitting.value = true;
const result = await fetchSplitRequirement(payload);
@@ -143,6 +144,10 @@ watch(
model.value = createDefaultModel();
if (props.parentRequirement?.category) {
model.value.category = props.parentRequirement.category;
}
await nextTick();
formRef.value?.clearValidate();
}
@@ -169,32 +174,8 @@ watch(
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="子需求标题" prop="title">
<ElInput v-model="model.title" clearable maxlength="256" placeholder="请输入子需求标题" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="预期完成时间" prop="completionDate" style="width: 100%">
<ElDatePicker
v-model="model.completionDate"
type="datetime"
class="w-full"
placeholder="选择预期完成时间"
value-format="x"
style="width: 100%"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="描述">
<ElInput
v-model="model.description"
type="textarea"
:rows="6"
maxlength="2000"
show-word-limit
placeholder="请输入需求描述"
/>
<ElFormItem label="子需求名称" prop="title">
<ElInput v-model="model.title" clearable maxlength="256" placeholder="请输入子需求名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -209,9 +190,16 @@ watch(
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="分类" prop="category">
<DictSelect v-model="model.category" :dict-code="categoryDictCode" filterable placeholder="请选择分类" />
<ElCol :span="24">
<ElFormItem label="内容">
<ElInput
v-model="model.description"
type="textarea"
:rows="6"
maxlength="2000"
show-word-limit
placeholder="请输入需求内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -235,6 +223,18 @@ watch(
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="所需工时" prop="workHours">
<ElInputNumber
v-model="model.workHours"
class="w-full"
:min="0"
:max="9999"
:precision="1"
placeholder="请输入所需工时"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="排序值">
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />