fix(产品需求、项目需求): 按照会议所说进行修改。
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, computed, inject, ref } from 'vue';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
|
||||
defineOptions({ name: 'ModuleTreeNode' });
|
||||
|
||||
@@ -32,10 +33,23 @@ const emit = defineEmits([
|
||||
'updateNewChildModuleName'
|
||||
]);
|
||||
|
||||
const { hasObjectAuth } = useAuth();
|
||||
const isRootModule = computed(() => props.module.id === props.rootModuleId);
|
||||
|
||||
const hasAnyActionPermission = computed(() => {
|
||||
if (isRootModule.value) {
|
||||
return hasObjectAuth('project:product:create');
|
||||
}
|
||||
return (
|
||||
hasObjectAuth('project:product:create') ||
|
||||
hasObjectAuth('project:product:update') ||
|
||||
hasObjectAuth('project:product:delete')
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -141,35 +155,27 @@ function handleToggle() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditing" class="module-tree-item__actions" @click.stop>
|
||||
<div v-if="!isEditing && hasAnyActionPermission" 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" />
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-auth="{ code: 'project:product:create', source: 'object' }"
|
||||
@click="handleStartAddChild"
|
||||
>
|
||||
<ElDropdownItem v-if="hasObjectAuth('project:product:create')" @click="handleStartAddChild">
|
||||
<div class="flex items-center gap-6px">
|
||||
<icon-ic-round-plus class="text-14px" />
|
||||
<span>新增子模块</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="!isRootModule"
|
||||
v-auth="{ code: 'project:product:update', source: 'object' }"
|
||||
@click="handleStartEdit"
|
||||
>
|
||||
<ElDropdownItem v-if="!isRootModule && hasObjectAuth('project:product:update')" @click="handleStartEdit">
|
||||
<div class="flex items-center gap-6px">
|
||||
<icon-mdi-pencil-outline class="text-14px" />
|
||||
<span>编辑</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="!isRootModule && canDeleteModule"
|
||||
v-auth="{ code: 'project:product:delete', source: 'object' }"
|
||||
v-if="!isRootModule && canDeleteModule && hasObjectAuth('project:product:delete')"
|
||||
divided
|
||||
@click="handleDelete"
|
||||
>
|
||||
@@ -241,73 +247,112 @@ function handleToggle() {
|
||||
.module-tree-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.module-tree-item {
|
||||
position: relative;
|
||||
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%);
|
||||
gap: 8px;
|
||||
min-height: 36px;
|
||||
padding: 6px 12px;
|
||||
padding-left: 16px;
|
||||
border-radius: 8px;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.module-tree-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 0;
|
||||
border-radius: 0 2px 2px 0;
|
||||
background-color: transparent;
|
||||
transition:
|
||||
height 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.module-tree-item:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgb(148 163 184 / 56%);
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.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;
|
||||
background-color: #f0fdfa;
|
||||
color: #0d9488;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.module-tree-item.is-root:not(.is-active) .module-tree-item__icon {
|
||||
color: rgb(13 148 136 / 80%);
|
||||
.module-tree-item.is-active::before {
|
||||
height: 60%;
|
||||
background-color: #14b8a6;
|
||||
}
|
||||
|
||||
.module-tree-item.is-root {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.module-tree-item.is-root:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.module-tree-item.is-root.is-active {
|
||||
background-color: #f0fdfa;
|
||||
color: #0d9488;
|
||||
}
|
||||
|
||||
.module-tree-item--new {
|
||||
border-style: dashed;
|
||||
border-color: rgb(148 163 184 / 56%);
|
||||
border: 1px dashed #cbd5e1;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.module-tree-item__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
color: rgb(100 116 139 / 80%);
|
||||
.module-tree-item--new:hover {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.module-tree-item__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: transform 0.2s ease;
|
||||
color: rgb(148 163 184);
|
||||
color: #94a3b8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.module-tree-item__toggle:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.module-tree-item__toggle.is-expanded svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.module-tree-item__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.module-tree-item.is-active .module-tree-item__icon {
|
||||
color: #14b8a6;
|
||||
}
|
||||
|
||||
.module-tree-item__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -326,7 +371,7 @@ function handleToggle() {
|
||||
}
|
||||
|
||||
.module-tree-item__input :deep(.el-input__inner) {
|
||||
height: 28px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.module-tree-item__actions {
|
||||
@@ -334,7 +379,7 @@ function handleToggle() {
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.module-tree-item:hover .module-tree-item__actions {
|
||||
@@ -347,5 +392,10 @@ function handleToggle() {
|
||||
|
||||
.module-tree-item__more-btn {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.module-tree-item__more-btn:hover {
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,7 +66,7 @@ const rules = computed(() => {
|
||||
}
|
||||
|
||||
if (isDispatchAction.value) {
|
||||
baseRules.implementProjectId = [createRequiredRule('请选择实现项目')];
|
||||
baseRules.implementProjectId = [createRequiredRule('请选择关联项目')];
|
||||
}
|
||||
|
||||
if (isTerminalAction.value) {
|
||||
@@ -136,8 +136,8 @@ async function handleSubmit() {
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="isDispatchAction" label="实现项目" prop="implementProjectId">
|
||||
<ElSelect v-model="model.implementProjectId" class="w-full" filterable placeholder="请选择实现项目(必选)">
|
||||
<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.id" :label="item.projectName" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchCreateRequirement, fetchGetRequirementModuleTree } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
@@ -47,6 +48,31 @@ const priorityOptions = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||
return {
|
||||
text,
|
||||
value: () => {
|
||||
const date = new Date();
|
||||
mutator(date);
|
||||
return date;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const expectedTimeShortcuts = [
|
||||
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||
];
|
||||
|
||||
interface Model {
|
||||
title: string;
|
||||
description: string | null;
|
||||
@@ -55,9 +81,9 @@ interface Model {
|
||||
moduleId: string;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
expectedTime: string | null;
|
||||
proposerId: string;
|
||||
currentHandlerUserId: string;
|
||||
workHours: number | null;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
@@ -93,18 +119,12 @@ const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() =
|
||||
return options;
|
||||
});
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
title: [createRequiredRule('请输入需求名称')],
|
||||
category: [createRequiredRule('请选择需求类型')],
|
||||
priority: [createRequiredRule('请选择优先级')],
|
||||
proposerId: [createRequiredRule('请选择提出人')],
|
||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
||||
workHours: [createRequiredRule('请输入所需工时')]
|
||||
currentHandlerUserId: [createRequiredRule('请选择负责人')]
|
||||
} satisfies Record<string, App.Global.FormRule[]>;
|
||||
|
||||
const leftColRef = ref<HTMLElement>();
|
||||
@@ -146,9 +166,9 @@ function createDefaultModel(): Model {
|
||||
moduleId: props.defaultModuleId || '0',
|
||||
category: '功能需求',
|
||||
priority: 1,
|
||||
expectedTime: null,
|
||||
proposerId: '',
|
||||
currentHandlerUserId: '',
|
||||
workHours: null,
|
||||
sort: 0
|
||||
};
|
||||
}
|
||||
@@ -183,12 +203,12 @@ async function handleSubmit() {
|
||||
attachments: [...model.value.attachments],
|
||||
category: model.value.category,
|
||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||
expectedTime: model.value.expectedTime,
|
||||
proposerId: model.value.proposerId,
|
||||
proposerNickname,
|
||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||
currentHandlerUserNickname,
|
||||
implementProjectId: null,
|
||||
workHours: model.value.workHours || 0,
|
||||
sort: model.value.sort
|
||||
};
|
||||
|
||||
@@ -272,14 +292,17 @@ watch(
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否需要评审">
|
||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
||||
<ElOption
|
||||
<ElRadioGroup v-model="model.reviewRequired">
|
||||
<ElRadio
|
||||
v-for="item in reviewRequiredOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
border
|
||||
style="width: 165px"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="优先级" prop="priority">
|
||||
@@ -288,17 +311,6 @@ watch(
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="所需工时" prop="workHours">
|
||||
<ElInputNumber
|
||||
v-model="model.workHours"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="1"
|
||||
placeholder="请输入所需工时"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="需求类型" prop="category">
|
||||
<DictSelect
|
||||
v-model="model.category"
|
||||
@@ -334,9 +346,20 @@ watch(
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="排序值">
|
||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
||||
<ElFormItem label="预期完成时间">
|
||||
<ElDatePicker
|
||||
v-model="model.expectedTime"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择预期完成时间"
|
||||
:shortcuts="expectedTimeShortcuts"
|
||||
class="requirement-operate-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- <ElFormItem label="排序值">-->
|
||||
<!-- <ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />-->
|
||||
<!-- </ElFormItem>-->
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
|
||||
@@ -397,4 +420,8 @@ watch(
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
fetchGetProjectListByProductId,
|
||||
fetchGetRequirement,
|
||||
@@ -65,12 +66,12 @@ interface Model {
|
||||
moduleId: string;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
expectedTime: string | null;
|
||||
proposerId: string;
|
||||
proposerNickname: string;
|
||||
currentHandlerUserId: string;
|
||||
currentHandlerUserNickname: string;
|
||||
implementProjectId: string | null;
|
||||
workHours: number | null;
|
||||
sort: number;
|
||||
lastStatusReason: string;
|
||||
}
|
||||
@@ -147,6 +148,26 @@ const reviewRequiredOptions = [
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||
return {
|
||||
text,
|
||||
value: () => {
|
||||
const date = new Date();
|
||||
mutator(date);
|
||||
return date;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const expectedTimeShortcuts = [
|
||||
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||
];
|
||||
|
||||
const rules = computed(() => {
|
||||
const baseRules: Record<string, App.Global.FormRule[]> = {
|
||||
title: isEditMode.value ? [createRequiredRule('请输入需求名称')] : [],
|
||||
@@ -198,12 +219,12 @@ function createDefaultModel(): Model {
|
||||
moduleId: '0',
|
||||
category: '',
|
||||
priority: 1,
|
||||
expectedTime: null,
|
||||
proposerId: '',
|
||||
proposerNickname: '',
|
||||
currentHandlerUserId: '',
|
||||
currentHandlerUserNickname: '',
|
||||
implementProjectId: null,
|
||||
workHours: null,
|
||||
sort: 0,
|
||||
lastStatusReason: ''
|
||||
};
|
||||
@@ -239,12 +260,12 @@ async function handleSubmit() {
|
||||
attachments: [...model.value.attachments],
|
||||
category: model.value.category,
|
||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||
expectedTime: model.value.expectedTime,
|
||||
proposerId: model.value.proposerId,
|
||||
proposerNickname: model.value.proposerNickname,
|
||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||
currentHandlerUserNickname: handler?.userNickname || model.value.currentHandlerUserNickname,
|
||||
implementProjectId: model.value.implementProjectId,
|
||||
workHours: model.value.workHours || 0,
|
||||
sort: model.value.sort
|
||||
};
|
||||
|
||||
@@ -304,17 +325,30 @@ function transformRequirementData(data: Api.Product.Requirement): typeof model.v
|
||||
moduleId: data.moduleId || '0',
|
||||
category: data.category || '',
|
||||
priority: data.priority ?? null,
|
||||
expectedTime: formatExpectedTime(data.expectedTime),
|
||||
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 || ''
|
||||
};
|
||||
}
|
||||
|
||||
function formatExpectedTime(value?: string | number[] | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const [year, month, day] = value;
|
||||
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return dayjs(value).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
async function loadRequirementDetail() {
|
||||
if (!props.productId || !props.requirement?.id) {
|
||||
return;
|
||||
@@ -402,21 +436,6 @@ watch(
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<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>
|
||||
|
||||
<ElFormItem label="需求类型" prop="category">
|
||||
<ReadonlyField :value="getCategoryLabel(model.category) || '--'" />
|
||||
</ElFormItem>
|
||||
@@ -447,24 +466,37 @@ watch(
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="isViewMode" label="实现项目">
|
||||
<ElFormItem v-if="isViewMode" label="关联项目">
|
||||
<ReadonlyField :value="projectOptionsMap.get(model.implementProjectId || '') || '--'" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="排序值">
|
||||
<template v-if="isViewMode">
|
||||
<ReadonlyField :value="model.sort" />
|
||||
</template>
|
||||
<ElInputNumber
|
||||
<ElFormItem label="预期完成时间">
|
||||
<ReadonlyField v-if="isViewMode" :value="model.expectedTime || '--'" />
|
||||
<ElDatePicker
|
||||
v-else
|
||||
v-model="model.sort"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
placeholder="请输入排序值"
|
||||
v-model="model.expectedTime"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择预期完成时间"
|
||||
:shortcuts="expectedTimeShortcuts"
|
||||
class="requirement-operate-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- <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>-->
|
||||
|
||||
<ElFormItem v-if="isViewMode && model.lastStatusReason" label="状态变更原因">
|
||||
<div class="requirement-operate-dialog__readonly-textarea">{{ model.lastStatusReason }}</div>
|
||||
</ElFormItem>
|
||||
@@ -528,7 +560,7 @@ watch(
|
||||
.requirement-operate-dialog__readonly-textarea {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
min-height: 65px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
@@ -545,4 +577,8 @@ watch(
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -216,15 +216,11 @@ async function handleDeleteModule(module: Api.Product.RequirementModule) {
|
||||
if (!currentObjectId.value) return;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除模块 "${module.moduleName}" 吗?该模块下的所有需求将被一并删除。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确认删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
await ElMessageBox.confirm(`确定要删除模块 "${module.moduleName}" 吗?`, '删除确认', {
|
||||
confirmButtonText: '确认删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
@@ -310,12 +306,12 @@ defineExpose({
|
||||
|
||||
.requirement-module-tree-card :deep(.el-card__header) {
|
||||
padding: 12px 16px;
|
||||
border-bottom: none;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.requirement-module-tree-card :deep(.el-card__body) {
|
||||
padding: 0 16px 16px;
|
||||
height: calc(100% - 48px);
|
||||
padding: 12px 8px;
|
||||
height: calc(100% - 49px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -326,68 +322,34 @@ defineExpose({
|
||||
}
|
||||
|
||||
.module-tree-header__title {
|
||||
color: rgb(15 23 42 / 94%);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.module-tree-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.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-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.module-tree-item:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgb(148 163 184 / 56%);
|
||||
.module-tree-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.module-tree-item--new {
|
||||
border-style: dashed;
|
||||
border-color: rgb(148 163 184 / 56%);
|
||||
.module-tree-list::-webkit-scrollbar-thumb {
|
||||
background-color: #e2e8f0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.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;
|
||||
.module-tree-list::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #cbd5e1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, h, onMounted, ref } from 'vue';
|
||||
import { RDMS_REQ_SOURCE_TYPE_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchGetRequirementStatusDict } from '@/service/api';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
|
||||
import TableSearchFields from '@/components/custom/table-search-fields.vue';
|
||||
import MemberSelectOption from './member-select-option.vue';
|
||||
|
||||
defineOptions({ name: 'RequirementSearch' });
|
||||
@@ -21,7 +20,7 @@ interface Props {
|
||||
priorityDictCode: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
@@ -45,6 +44,21 @@ const sourceTypeOptions = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const memberSelectOptions = computed(() => {
|
||||
return props.memberOptions.map(item => ({
|
||||
label: item.nickname,
|
||||
value: item.id,
|
||||
roleName: item.roleName
|
||||
}));
|
||||
});
|
||||
|
||||
function renderMemberOption(option: { label: string; value: string | number; roleName?: string }) {
|
||||
return h(MemberSelectOption, {
|
||||
nickname: option.label,
|
||||
roleName: option.roleName || ''
|
||||
});
|
||||
}
|
||||
|
||||
async function loadStatusOptions() {
|
||||
const { error, data } = await fetchGetRequirementStatusDict();
|
||||
|
||||
@@ -59,77 +73,58 @@ async function loadStatusOptions() {
|
||||
}));
|
||||
}
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadStatusOptions();
|
||||
});
|
||||
|
||||
const fields = computed(() => [
|
||||
{
|
||||
key: 'title',
|
||||
label: '需求名称',
|
||||
type: 'input' as const,
|
||||
placeholder: '输入需求名称'
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
label: '优先级',
|
||||
type: 'dict' as const,
|
||||
dictCode: props.priorityDictCode,
|
||||
placeholder: '筛选优先级'
|
||||
},
|
||||
{
|
||||
key: 'statusCode',
|
||||
label: '状态',
|
||||
type: 'select' as const,
|
||||
placeholder: '筛选状态',
|
||||
options: requirementStatusOptions.value
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
label: '需求类型',
|
||||
type: 'dict' as const,
|
||||
dictCode: props.categoryDictCode,
|
||||
placeholder: '筛选需求类型'
|
||||
},
|
||||
{
|
||||
key: 'sourceType',
|
||||
label: '需求来源',
|
||||
type: 'select' as const,
|
||||
placeholder: '筛选需求来源',
|
||||
options: sourceTypeOptions.value
|
||||
},
|
||||
{
|
||||
key: 'currentHandlerUserId',
|
||||
label: '负责人',
|
||||
type: 'select' as const,
|
||||
placeholder: '筛选负责人',
|
||||
options: memberSelectOptions.value,
|
||||
renderOption: renderMemberOption
|
||||
}
|
||||
]);
|
||||
</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="需求来源">
|
||||
<ElSelect v-model="model.sourceType" clearable placeholder="筛选需求来源">
|
||||
<ElOption v-for="item in sourceTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</TableSearchPanel>
|
||||
<TableSearchFields v-model="model" :fields="fields" :columns="3" @search="emit('search')" @reset="emit('reset')" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchSplitRequirement } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
@@ -46,6 +47,31 @@ const priorityOptions = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
function buildExpectedTimeShortcut(text: string, mutator: (date: Date) => void) {
|
||||
return {
|
||||
text,
|
||||
value: () => {
|
||||
const date = new Date();
|
||||
mutator(date);
|
||||
return date;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const expectedTimeShortcuts = [
|
||||
buildExpectedTimeShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
||||
buildExpectedTimeShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
||||
buildExpectedTimeShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
||||
buildExpectedTimeShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
||||
buildExpectedTimeShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
||||
buildExpectedTimeShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
||||
];
|
||||
|
||||
interface Model {
|
||||
title: string;
|
||||
description: string | null;
|
||||
@@ -53,8 +79,8 @@ interface Model {
|
||||
reviewRequired: number;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
expectedTime: string | null;
|
||||
currentHandlerUserId: string;
|
||||
workHours: number | null;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
@@ -66,16 +92,10 @@ const memberUserOptions = computed(() => {
|
||||
return props.memberOptions.filter(m => m.status === 0);
|
||||
});
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
title: [createRequiredRule('请输入子需求名称')],
|
||||
priority: [createRequiredRule('请选择优先级')],
|
||||
currentHandlerUserId: [createRequiredRule('请选择负责人')],
|
||||
workHours: [createRequiredRule('请输入所需工时')]
|
||||
currentHandlerUserId: [createRequiredRule('请选择负责人')]
|
||||
} satisfies Record<string, App.Global.FormRule[]>;
|
||||
|
||||
const leftColRef = ref<HTMLElement>();
|
||||
@@ -116,8 +136,8 @@ function createDefaultModel(): Model {
|
||||
reviewRequired: 0,
|
||||
category: '',
|
||||
priority: 1,
|
||||
expectedTime: null,
|
||||
currentHandlerUserId: '',
|
||||
workHours: null,
|
||||
sort: 0
|
||||
};
|
||||
}
|
||||
@@ -153,8 +173,8 @@ async function handleSubmit() {
|
||||
reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired,
|
||||
category: model.value.category,
|
||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||
expectedTime: model.value.expectedTime,
|
||||
currentHandlerUserId: model.value.currentHandlerUserId,
|
||||
workHours: model.value.workHours || 0,
|
||||
sort: model.value.sort
|
||||
};
|
||||
|
||||
@@ -192,6 +212,10 @@ watch(
|
||||
model.value.currentHandlerUserId = props.parentRequirement.currentHandlerUserId;
|
||||
}
|
||||
|
||||
if (props.parentRequirement?.expectedTime) {
|
||||
model.value.expectedTime = props.parentRequirement.expectedTime;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
attachmentUploaderRef.value?.initSession();
|
||||
richTextEditorRef.value?.initSession();
|
||||
@@ -227,14 +251,17 @@ watch(
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否需要评审">
|
||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
||||
<ElOption
|
||||
<ElRadioGroup v-model="model.reviewRequired">
|
||||
<ElRadio
|
||||
v-for="item in reviewRequiredOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
border
|
||||
style="width: 165px"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="优先级" prop="priority">
|
||||
@@ -243,17 +270,6 @@ watch(
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="所需工时" prop="workHours">
|
||||
<ElInputNumber
|
||||
v-model="model.workHours"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="1"
|
||||
placeholder="请输入所需工时"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
|
||||
<ElOption
|
||||
@@ -267,9 +283,20 @@ watch(
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="排序值">
|
||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
||||
<ElFormItem label="预期完成时间">
|
||||
<ElDatePicker
|
||||
v-model="model.expectedTime"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择预期完成时间"
|
||||
:shortcuts="expectedTimeShortcuts"
|
||||
class="requirement-operate-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- <ElFormItem label="排序值">-->
|
||||
<!-- <ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />-->
|
||||
<!-- </ElFormItem>-->
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
|
||||
@@ -330,4 +357,8 @@ watch(
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user