fix(产品需求): 完善产品需求的诸多细节。

This commit is contained in:
dk
2026-05-09 18:15:10 +08:00
parent 5947157f89
commit 28c47b14a3
13 changed files with 337 additions and 245 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, inject, ref, type Ref } from 'vue';
import { type Ref, computed, inject, ref } from 'vue';
defineOptions({ name: 'ModuleTreeNode' });
@@ -40,7 +40,9 @@ 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 isCollapsed = computed(() =>
hasChildren.value && props.module.id ? collapsedModuleIds.value.has(props.module.id) : false
);
const hasRequirements = computed(() => {
const moduleId = props.module.id;
@@ -111,7 +113,11 @@ function handleToggle() {
:style="indentStyle"
@click="handleClick"
>
<div class="module-tree-item__toggle" :class="{ 'is-expanded': hasChildren && !isCollapsed }" @click.stop="handleToggle">
<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">

View File

@@ -14,6 +14,7 @@ defineOptions({ name: 'RequirementActionDialog' });
interface Props {
action: Api.Product.RequirementLifecycleAction | null;
requirementTitle: string;
projectOptions: Api.Project.Project[];
}
const props = defineProps<Props>();
@@ -57,8 +58,6 @@ const reviewChoiceOptions = [
{ label: '不需要评审', value: 'claim_to_dispatch', description: '认领后直接进入分流' }
];
const projectOptions = [{ label: 'NPQS-10086', value: '202642910086' }];
const rules = computed(() => {
const baseRules: Record<string, App.Global.FormRule[]> = {};
@@ -118,8 +117,7 @@ async function handleSubmit() {
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElFormItem :label="`需求名称:${requirementTitle}`">
</ElFormItem>
<ElFormItem :label="`需求名称:${requirementTitle}`"></ElFormItem>
<ElFormItem v-if="isClaimAction" label="是否需要评审" prop="reviewChoice">
<ElRadioGroup v-model="model.reviewChoice" class="business-form-radio-group">
@@ -134,7 +132,7 @@ async function handleSubmit() {
<ElFormItem v-if="isDispatchAction" label="实现项目" prop="implementProjectId">
<ElSelect v-model="model.implementProjectId" class="w-full" filterable placeholder="请选择实现项目(必选)">
<ElOption v-for="item in projectOptions" :key="item.value" :label="item.label" :value="item.value" />
<ElOption v-for="item in projectOptions" :key="item.id" :label="item.projectName" :value="item.id" />
</ElSelect>
</ElFormItem>

View File

@@ -4,6 +4,7 @@ import { fetchCreateRequirement, fetchGetRequirementModuleTree } from '@/service
import { useForm, useFormRules } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
import DictSelect from '@/components/custom/dict-select.vue';
import MemberSelectOption from './member-select-option.vue';
@@ -216,6 +217,23 @@ watch(
<ElInput v-model="model.title" clearable maxlength="256" placeholder="请输入需求名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<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="24">
<ElFormItem label="内容">
<BusinessRichTextEditor
v-model="model.description"
height="240px"
upload-directory="requirement"
placeholder="请输入需求内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="是否需要评审">
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
@@ -228,35 +246,6 @@ watch(
</ElSelect>
</ElFormItem>
</ElCol>
<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="模块">
<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">
<ElFormItem label="优先级" prop="priority">
<ElSelect v-model="model.priority" class="w-full" filterable placeholder="请选择优先级">
@@ -264,6 +253,28 @@ 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="需求类型" prop="category">
<DictSelect
v-model="model.category"
:dict-code="categoryDictCode"
filterable
placeholder="请选择需求类型"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="提出人" prop="proposerId">
<ElSelect v-model="model.proposerId" class="w-full" filterable placeholder="请选择提出人">
@@ -292,18 +303,6 @@ 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,13 +1,19 @@
<script setup lang="ts">
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 { computed, nextTick, ref, watch } from 'vue';
import {
fetchGetProjectListByProductId,
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 BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.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';
@@ -32,11 +38,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 => ({
@@ -65,6 +71,7 @@ interface Model {
const loading = ref(false);
const submitting = ref(false);
const moduleTree = ref<Api.Product.RequirementModule[]>([]);
const projectOptions = ref<Api.Project.Project[]>([]);
const model = ref<Model>(createDefaultModel());
@@ -101,6 +108,10 @@ const moduleLabelMap = computed(() => {
return map;
});
const projectOptionsMap = computed(() => {
return new Map(projectOptions.value.map(item => [String(item.id), item.projectName]));
});
const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() => {
const options: Array<{ label: string; value: string }> = [];
@@ -125,8 +136,8 @@ const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() =
});
const reviewRequiredOptions = [
{label: '不需要', value: 0},
{label: '需要', value: 1}
{ label: '不需要', value: 0 },
{ label: '需要', value: 1 }
];
const rules = computed(() => {
@@ -195,7 +206,7 @@ async function handleSubmit() {
sort: model.value.sort
};
const {error} = await fetchUpdateRequirement(updatePayload);
const { error } = await fetchUpdateRequirement(updatePayload);
submitting.value = false;
@@ -214,7 +225,7 @@ async function loadModuleTree() {
return;
}
const {error, data} = await fetchGetRequirementModuleTree(props.productId);
const { error, data } = await fetchGetRequirementModuleTree(props.productId);
if (error || !data) {
moduleTree.value = [];
@@ -224,6 +235,22 @@ async function loadModuleTree() {
moduleTree.value = data;
}
async function loadProjectOptions() {
if (!props.productId) {
projectOptions.value = [];
return;
}
const { error, data } = await fetchGetProjectListByProductId(props.productId);
if (error || !data) {
projectOptions.value = [];
return;
}
projectOptions.value = data;
}
async function loadRequirementDetail() {
if (!props.productId || !props.requirement?.id) {
return;
@@ -231,7 +258,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;
@@ -264,7 +291,7 @@ watch(
return;
}
await loadModuleTree();
await Promise.all([loadModuleTree(), loadProjectOptions()]);
if (props.requirement?.id) {
await loadRequirementDetail();
@@ -293,47 +320,33 @@ watch(
<ReadonlyField :value="model.title" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="是否需要评审">
<ReadonlyField :value="reviewRequiredOptions.find(opt => opt.value === model.reviewRequired)?.label"/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="内容">
<template v-if="isViewMode">
<div class="readonly-textarea">
{{ model.description || '--' }}
</div>
</template>
<ElInput
v-else
v-model="model.description"
type="textarea"
:rows="6"
maxlength="2000"
show-word-limit
placeholder="请输入需求内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="模块">
<template v-if="isViewMode">
<ReadonlyField :value="moduleLabelMap.get(model.moduleId) || '--'" />
</template>
<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"
/>
<ElOption v-for="item in flatModuleOptions" :key="item.value" :label="item.label" :value="item.value" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="内容">
<template v-if="isViewMode">
<div class="readonly-textarea" v-html="model.description || '--'"></div>
</template>
<BusinessRichTextEditor
v-else
v-model="model.description"
height="240px"
upload-directory="requirement"
placeholder="请输入需求内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="需求类型" prop="category">
<ReadonlyField :value="getCategoryLabel(model.category) || '--'"/>
<ElFormItem label="是否需要评审">
<ReadonlyField :value="reviewRequiredOptions.find(opt => opt.value === model.reviewRequired)?.label" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
@@ -344,36 +357,14 @@ 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"/>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="提出人" prop="proposerId">
<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) || '--'"/>
</template>
<ElSelect v-else v-model="model.currentHandlerUserId" 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>
<ElOption v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="所需工时">
<template v-if="isViewMode">
<ReadonlyField :value="model.workHours != null ? `${model.workHours}小时` : '--'"/>
<ReadonlyField :value="model.workHours != null ? `${model.workHours}小时` : '--'" />
</template>
<ElInputNumber
v-else
@@ -386,17 +377,56 @@ watch(
/>
</ElFormItem>
</ElCol>
<ElCol v-if="isViewMode" :span="12">
<ElCol :span="12">
<ElFormItem label="需求类型" prop="category">
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="提出人" prop="proposerId">
<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) || '--'" />
</template>
<ElSelect v-else v-model="model.currentHandlerUserId" 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>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="实现项目">
<ReadonlyField :value="model.implementProjectId || '--'"/>
<template v-if="isViewMode">
<ReadonlyField :value="projectOptionsMap.get(model.implementProjectId || '') || '--'" />
</template>
<ElSelect
v-else
v-model="model.implementProjectId"
class="w-full"
filterable
clearable
placeholder="请选择实现项目"
>
<ElOption v-for="item in projectOptions" :key="item.id" :label="item.projectName" :value="item.id" />
</ElSelect>
</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

@@ -4,6 +4,7 @@ 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 BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
import MemberSelectOption from './member-select-option.vue';
defineOptions({ name: 'RequirementSplitDialog' });
@@ -192,12 +193,10 @@ watch(
</ElCol>
<ElCol :span="24">
<ElFormItem label="内容">
<ElInput
<BusinessRichTextEditor
v-model="model.description"
type="textarea"
:rows="6"
maxlength="2000"
show-word-limit
height="240px"
upload-directory="requirement"
placeholder="请输入需求内容"
/>
</ElFormItem>
@@ -209,6 +208,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="负责人" prop="currentHandlerUserId">
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
@@ -223,18 +234,6 @@ 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="请输入排序值" />