feat(产品需求): 实现产品需求相关代码。
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
defineOptions({ name: 'MemberSelectOption' });
|
||||
|
||||
interface Props {
|
||||
nickname: string;
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="member-select-option">
|
||||
<span class="member-select-option__name">{{ nickname }}</span>
|
||||
<ElTag type="info" size="small" class="member-select-option__role">{{ roleName }}</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.member-select-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-select-option__name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-select-option__role {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
type RequirementStatusActionCode,
|
||||
isRequirementActionNeedProject,
|
||||
isRequirementActionNeedReviewChoice,
|
||||
isRequirementActionTerminal
|
||||
} from '../shared/requirement-master-data';
|
||||
|
||||
defineOptions({ name: 'RequirementActionDialog' });
|
||||
|
||||
interface Props {
|
||||
action: Api.Product.RequirementLifecycleAction | null;
|
||||
requirementTitle: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted', payload: { actionCode: string; reason?: string; implementProjectId?: string }): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
interface Model {
|
||||
reviewChoice: string;
|
||||
implementProjectId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
const model = ref<Model>({ reviewChoice: '', implementProjectId: '', reason: '' });
|
||||
const submitting = ref(false);
|
||||
|
||||
const actionCode = computed(() => props.action?.actionCode as RequirementStatusActionCode | undefined);
|
||||
|
||||
const isClaimAction = computed(() =>
|
||||
actionCode.value ? isRequirementActionNeedReviewChoice(actionCode.value) : false
|
||||
);
|
||||
const isDispatchAction = computed(() => (actionCode.value ? isRequirementActionNeedProject(actionCode.value) : false));
|
||||
const isTerminalAction = computed(() => (actionCode.value ? isRequirementActionTerminal(actionCode.value) : false));
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (!props.action) return '';
|
||||
if (isClaimAction.value) return '认领需求';
|
||||
return props.action.actionName;
|
||||
});
|
||||
|
||||
const reviewChoiceOptions = [
|
||||
{ label: '需要评审', value: 'claim_to_review', description: '认领后进入评审流程' },
|
||||
{ label: '不需要评审', value: 'claim_to_dispatch', description: '认领后直接进入分流' }
|
||||
];
|
||||
|
||||
const projectOptions = [{ label: 'NPQS-10086', value: '202642910086' }];
|
||||
|
||||
const rules = computed(() => {
|
||||
const baseRules: Record<string, App.Global.FormRule[]> = {};
|
||||
|
||||
if (isClaimAction.value) {
|
||||
baseRules.reviewChoice = [createRequiredRule('请选择是否需要评审')];
|
||||
}
|
||||
|
||||
if (isDispatchAction.value) {
|
||||
baseRules.implementProjectId = [createRequiredRule('请选择实现项目')];
|
||||
}
|
||||
|
||||
if (isTerminalAction.value) {
|
||||
baseRules.reason = [createRequiredRule('请输入状态变更原因')];
|
||||
}
|
||||
|
||||
return baseRules;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (val) {
|
||||
model.value = { reviewChoice: '', implementProjectId: '', reason: '' };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const payload: { actionCode: string; reason?: string; implementProjectId?: string } = {
|
||||
actionCode: isClaimAction.value ? model.value.reviewChoice : props.action!.actionCode
|
||||
};
|
||||
|
||||
if (isDispatchAction.value) {
|
||||
payload.implementProjectId = model.value.implementProjectId;
|
||||
}
|
||||
|
||||
if (isTerminalAction.value) {
|
||||
payload.reason = model.value.reason.trim();
|
||||
}
|
||||
|
||||
emit('submitted', payload);
|
||||
submitting.value = false;
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
preset="sm"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElFormItem label="需求标题">
|
||||
<span class="text-14px">{{ requirementTitle }}</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="isClaimAction" label="是否需要评审" prop="reviewChoice">
|
||||
<ElRadioGroup v-model="model.reviewChoice" class="business-form-radio-group">
|
||||
<ElRadio v-for="option in reviewChoiceOptions" :key="option.value" :value="option.value">
|
||||
<div class="flex flex-col gap-2px">
|
||||
<span>{{ option.label }}</span>
|
||||
<span class="text-12px text-gray-400">{{ option.description }}</span>
|
||||
</div>
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<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" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="isTerminalAction" label="变更原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="model.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入状态变更原因(必填)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,294 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { fetchCreateRequirement, fetchGetRequirementModuleTree } 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: 'RequirementCreateDialog' });
|
||||
|
||||
interface Props {
|
||||
productId: string;
|
||||
defaultModuleId?: string;
|
||||
memberOptions: Api.Product.ProductMember[];
|
||||
categoryDictCode: string;
|
||||
priorityDictCode: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const { enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
|
||||
|
||||
const priorityOptions = computed(() => {
|
||||
return priorityDictData.value.map(item => ({
|
||||
label: item.label,
|
||||
value: Number(item.value)
|
||||
}));
|
||||
});
|
||||
|
||||
interface Model {
|
||||
title: string;
|
||||
description: string;
|
||||
reviewRequired: number;
|
||||
completionDate: string;
|
||||
moduleId: string;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
proposerId: string;
|
||||
currentHandlerUserId: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
const submitting = ref(false);
|
||||
const loading = ref(false);
|
||||
const moduleTree = ref<Api.Product.RequirementModule[]>([]);
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const memberUserOptions = computed(() => {
|
||||
return props.memberOptions.filter(m => m.status === 0);
|
||||
});
|
||||
|
||||
const moduleTreeProps = {
|
||||
label: 'moduleName',
|
||||
value: 'id',
|
||||
children: 'children'
|
||||
};
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
title: [createRequiredRule('请输入需求标题')],
|
||||
category: [createRequiredRule('请选择分类')],
|
||||
priority: [createRequiredRule('请选择优先级')],
|
||||
proposerId: [createRequiredRule('请选择提出人')],
|
||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
||||
completionDate: [createRequiredRule('请选择预期完成时间')]
|
||||
} satisfies Record<string, App.Global.FormRule[]>;
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
reviewRequired: 0,
|
||||
completionDate: '',
|
||||
moduleId: props.defaultModuleId || '0',
|
||||
category: '功能需求',
|
||||
priority: 1,
|
||||
proposerId: '',
|
||||
currentHandlerUserId: '',
|
||||
sort: 0
|
||||
};
|
||||
}
|
||||
|
||||
function getNullableText(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
if (!props.productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Api.Product.SaveRequirementParams = {
|
||||
productId: props.productId,
|
||||
moduleId: model.value.moduleId || '0',
|
||||
reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired,
|
||||
title: model.value.title.trim(),
|
||||
description: getNullableText(model.value.description),
|
||||
category: model.value.category,
|
||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||
proposerId: model.value.proposerId,
|
||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||
implementProjectId: null,
|
||||
completionDate: model.value.completionDate,
|
||||
sort: model.value.sort
|
||||
};
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const result = await fetchCreateRequirement(payload);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('需求新增成功');
|
||||
closeDialog();
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
async function loadModuleTree() {
|
||||
if (!props.productId) {
|
||||
moduleTree.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, data } = await fetchGetRequirementModuleTree(props.productId);
|
||||
|
||||
if (error || !data) {
|
||||
moduleTree.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
moduleTree.value = data;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value = createDefaultModel();
|
||||
await loadModuleTree();
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="新增需求"
|
||||
preset="lg"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<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>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="是否需要评审">
|
||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
||||
<ElOption
|
||||
v-for="item in reviewRequiredOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</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="请选择所属模块"
|
||||
/>
|
||||
</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="请选择优先级">
|
||||
<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">
|
||||
<ElSelect 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>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||
<ElSelect 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="排序值">
|
||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,433 @@
|
||||
<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 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' });
|
||||
|
||||
type DialogMode = 'view' | 'edit';
|
||||
|
||||
interface Props {
|
||||
mode: DialogMode;
|
||||
requirement: Api.Product.Requirement | null;
|
||||
productId: string;
|
||||
memberOptions: Api.Product.ProductMember[];
|
||||
categoryDictCode: string;
|
||||
priorityDictCode: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted', requirementId?: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const { getLabel: getCategoryLabel } = useDict(() => props.categoryDictCode);
|
||||
const { getLabel: getPriorityLabel, enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
|
||||
|
||||
const priorityOptions = computed(() => {
|
||||
return priorityDictData.value.map(item => ({
|
||||
label: item.label,
|
||||
value: Number(item.value)
|
||||
}));
|
||||
});
|
||||
|
||||
interface Model {
|
||||
title: string;
|
||||
description: string;
|
||||
reviewRequired: number;
|
||||
completionDate: string;
|
||||
moduleId: string;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
proposerId: string;
|
||||
currentHandlerUserId: string;
|
||||
implementProjectId: string | null;
|
||||
sort: number;
|
||||
lastStatusReason: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const moduleTree = ref<Api.Product.RequirementModule[]>([]);
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const isViewMode = computed(() => props.mode === 'view');
|
||||
const isEditMode = computed(() => props.mode === 'edit');
|
||||
const dialogTitle = computed(() => {
|
||||
if (isViewMode.value) {
|
||||
return '查看需求';
|
||||
}
|
||||
return '编辑需求';
|
||||
});
|
||||
|
||||
const memberUserOptions = computed(() => {
|
||||
return props.memberOptions.filter(m => m.status === 0);
|
||||
});
|
||||
|
||||
const memberLabelMap = computed(() => {
|
||||
return new Map(memberUserOptions.value.map(item => [String(item.userId), item.userNickname]));
|
||||
});
|
||||
|
||||
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);
|
||||
if (module.children?.length) {
|
||||
traverse(module.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(moduleTree.value);
|
||||
return map;
|
||||
});
|
||||
|
||||
const moduleTreeProps = {
|
||||
label: 'moduleName',
|
||||
value: 'id',
|
||||
children: 'children'
|
||||
};
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
const rules = computed(() => {
|
||||
const baseRules: Record<string, App.Global.FormRule[]> = {
|
||||
title: isEditMode.value ? [createRequiredRule('请输入需求标题')] : [],
|
||||
category: isEditMode.value ? [createRequiredRule('请选择分类')] : [],
|
||||
priority: isEditMode.value ? [createRequiredRule('请选择优先级')] : [],
|
||||
proposerId: isEditMode.value ? [createRequiredRule('请选择提出人')] : [],
|
||||
currentHandlerUserId: isEditMode.value ? [createRequiredRule('请选择负责人')] : [],
|
||||
completionDate: isEditMode.value ? [createRequiredRule('请选择预期完成时间')] : []
|
||||
};
|
||||
|
||||
return baseRules;
|
||||
});
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
reviewRequired: 0,
|
||||
completionDate: '',
|
||||
moduleId: '0',
|
||||
category: '',
|
||||
priority: 1,
|
||||
proposerId: '',
|
||||
currentHandlerUserId: '',
|
||||
implementProjectId: null,
|
||||
sort: 0,
|
||||
lastStatusReason: ''
|
||||
};
|
||||
}
|
||||
|
||||
function getNullableText(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
if (!props.productId || !props.requirement?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const updatePayload: Api.Product.UpdateRequirementParams = {
|
||||
id: props.requirement.id,
|
||||
productId: props.productId,
|
||||
moduleId: model.value.moduleId || '0',
|
||||
reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired,
|
||||
title: model.value.title.trim(),
|
||||
description: getNullableText(model.value.description),
|
||||
category: model.value.category,
|
||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||
proposerId: model.value.proposerId,
|
||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||
implementProjectId: model.value.implementProjectId,
|
||||
completionDate: model.value.completionDate,
|
||||
sort: model.value.sort
|
||||
};
|
||||
|
||||
const { error } = await fetchUpdateRequirement(updatePayload);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('需求更新成功');
|
||||
closeDialog();
|
||||
emit('submitted', props.requirement.id);
|
||||
}
|
||||
|
||||
async function loadModuleTree() {
|
||||
if (!props.productId) {
|
||||
moduleTree.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, data } = await fetchGetRequirementModuleTree(props.productId);
|
||||
|
||||
if (error || !data) {
|
||||
moduleTree.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
moduleTree.value = data;
|
||||
}
|
||||
|
||||
async function loadRequirementDetail() {
|
||||
if (!props.productId || !props.requirement?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const { error, data } = await fetchGetRequirement(props.requirement.id, props.productId);
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (error || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value = {
|
||||
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 || '',
|
||||
currentHandlerUserId: data.currentHandlerUserId || '',
|
||||
implementProjectId: data.implementProjectId || null,
|
||||
sort: data.sort ?? 0,
|
||||
lastStatusReason: data.lastStatusReason || ''
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await loadModuleTree();
|
||||
|
||||
if (props.requirement?.id) {
|
||||
await loadRequirementDetail();
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
preset="lg"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
:show-footer="isEditMode"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
</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="是否需要评审">
|
||||
<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="请选择所属模块"
|
||||
/>
|
||||
</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>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="优先级" prop="priority">
|
||||
<template v-if="isViewMode">
|
||||
<ReadonlyField
|
||||
:value="model.priority !== null ? getPriorityLabel(String(model.priority)) || '--' : '--'"
|
||||
/>
|
||||
</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">
|
||||
<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>
|
||||
</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 || '--'" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="排序值">
|
||||
<template v-if="isViewMode">
|
||||
<ReadonlyField :value="model.sort" />
|
||||
</template>
|
||||
<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">
|
||||
<ElFormItem label="状态变更原因">
|
||||
<div class="readonly-textarea">
|
||||
{{ model.lastStatusReason }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.readonly-textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
color: rgb(51 65 85 / 96%);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
115
src/views/product/requirement/modules/requirement-search.vue
Normal file
115
src/views/product/requirement/modules/requirement-search.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
||||
import MemberSelectOption from './member-select-option.vue';
|
||||
|
||||
defineOptions({ name: 'RequirementSearch' });
|
||||
|
||||
interface MemberUserOption {
|
||||
id: string;
|
||||
nickname: string;
|
||||
roleName?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
memberOptions: MemberUserOption[];
|
||||
categoryDictCode: string;
|
||||
priorityDictCode: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.Product.RequirementSearchParams>('model', { required: true });
|
||||
|
||||
const requirementStatusOptions = [
|
||||
{ label: '待确认', value: 'pending_confirm' },
|
||||
{ label: '待评审', value: 'pending_review' },
|
||||
{ label: '待分流', value: 'pending_dispatch' },
|
||||
{ label: '实施中', value: 'implementing' },
|
||||
{ label: '已验收', value: 'accepted' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '已拒绝', value: 'rejected' },
|
||||
{ label: '已取消', value: 'cancelled' }
|
||||
];
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="分类">
|
||||
<DictSelect
|
||||
v-model="model.category"
|
||||
:dict-code="categoryDictCode"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="筛选分类"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="优先级">
|
||||
<DictSelect v-model="model.priority" :dict-code="priorityDictCode" clearable placeholder="筛选优先级" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="状态">
|
||||
<ElSelect v-model="model.statusCode" clearable placeholder="筛选状态">
|
||||
<ElOption
|
||||
v-for="item in requirementStatusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="负责人">
|
||||
<ElSelect
|
||||
v-model="model.currentHandlerUserId"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="筛选负责人"
|
||||
:filter-method="(val: string) => val"
|
||||
>
|
||||
<ElOption v-for="item in memberOptions" :key="item.id" :label="item.nickname" :value="item.id">
|
||||
<MemberSelectOption :nickname="item.nickname" :role-name="item.roleName || ''" />
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="12" :sm="12">
|
||||
<ElFormItem label="来源类型">
|
||||
<DictSelect
|
||||
v-model="model.sourceType"
|
||||
:dict-code="RDMS_REQ_SOURCE_TYPE_DICT_CODE"
|
||||
clearable
|
||||
placeholder="筛选来源类型"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</TableSearchPanel>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
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' });
|
||||
|
||||
interface Props {
|
||||
parentRequirement: Api.Product.Requirement | null;
|
||||
productId: string;
|
||||
memberOptions: Api.Product.ProductMember[];
|
||||
categoryDictCode: string;
|
||||
priorityDictCode: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const { enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
|
||||
|
||||
const priorityOptions = computed(() => {
|
||||
return priorityDictData.value.map(item => ({
|
||||
label: item.label,
|
||||
value: Number(item.value)
|
||||
}));
|
||||
});
|
||||
|
||||
interface Model {
|
||||
title: string;
|
||||
description: string;
|
||||
reviewRequired: number;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
currentHandlerUserId: string;
|
||||
completionDate: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
const submitting = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const memberUserOptions = computed(() => {
|
||||
return props.memberOptions.filter(m => m.status === 0);
|
||||
});
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
title: [createRequiredRule('请输入子需求标题')],
|
||||
category: [createRequiredRule('请选择分类')],
|
||||
priority: [createRequiredRule('请选择优先级')],
|
||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
||||
completionDate: [createRequiredRule('请选择预期完成时间')]
|
||||
} satisfies Record<string, App.Global.FormRule[]>;
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
reviewRequired: 0,
|
||||
category: '',
|
||||
priority: 1,
|
||||
currentHandlerUserId: '',
|
||||
completionDate: '',
|
||||
sort: 0
|
||||
};
|
||||
}
|
||||
|
||||
function getNullableText(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
if (!props.productId || !props.parentRequirement?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Api.Product.SplitRequirementParams = {
|
||||
parentId: props.parentRequirement.id,
|
||||
productId: props.productId,
|
||||
moduleId: props.parentRequirement.moduleId,
|
||||
proposerId: props.parentRequirement.proposerId,
|
||||
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,
|
||||
sort: model.value.sort
|
||||
};
|
||||
|
||||
console.log('payload', payload);
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const result = await fetchSplitRequirement(payload);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success('需求拆分成功');
|
||||
closeDialog();
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value = createDefaultModel();
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="拆分需求"
|
||||
preset="lg"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElAlert
|
||||
v-if="parentRequirement"
|
||||
:title="`正在拆分需求:${parentRequirement.title}`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-16px"
|
||||
/>
|
||||
|
||||
<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>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="是否需要评审">
|
||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
||||
<ElOption
|
||||
v-for="item in reviewRequiredOptions"
|
||||
: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="请选择优先级">
|
||||
<ElOption v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||
<ElSelect 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="排序值">
|
||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user