feat(新增需求评审功能): 新增需求评审功能。
feat(动态切换对象域下的对象):对象域下的对象可以动态切换。 fix(产品需求、项目需求): 按照会议意见修改诸多细节。 fix(产品对象域的概览界面): 把假数据换成真实的需求统计数据。
This commit is contained in:
@@ -156,37 +156,31 @@ function handleToggle() {
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<ElTooltip v-if="hasObjectAuth('project:product:create')" content="新增子模块" placement="top">
|
||||
<ElButton link type="primary" class="module-tree-item__action-btn" @click="handleStartAddChild">
|
||||
<icon-mdi-plus class="text-14px" />
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<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 && 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 && hasObjectAuth('project:product:delete')"
|
||||
divided
|
||||
@click="handleDelete"
|
||||
>
|
||||
<div class="flex items-center gap-6px text-error">
|
||||
</ElTooltip>
|
||||
<ElTooltip v-if="!isRootModule && hasObjectAuth('project:product:update')" content="编辑" placement="top">
|
||||
<ElButton link type="primary" class="module-tree-item__action-btn" @click="handleStartEdit">
|
||||
<icon-mdi-pencil-outline class="text-14px" />
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
<ElPopconfirm
|
||||
v-if="!isRootModule && canDeleteModule && hasObjectAuth('project:product:delete')"
|
||||
title="确定删除该模块吗?"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<template #reference>
|
||||
<span class="inline-flex" @click.stop>
|
||||
<ElTooltip content="删除" placement="top">
|
||||
<ElButton link type="danger" class="module-tree-item__action-btn">
|
||||
<icon-mdi-delete-outline class="text-14px" />
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</span>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</ElPopconfirm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -390,12 +384,15 @@ function handleToggle() {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.module-tree-item__more-btn {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
.module-tree-item__action-btn {
|
||||
padding: 2px;
|
||||
min-width: auto;
|
||||
height: auto;
|
||||
margin-left: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.module-tree-item__more-btn:hover {
|
||||
background-color: #e2e8f0;
|
||||
.module-tree-item__action-btn:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,8 +5,7 @@ import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import {
|
||||
type RequirementStatusActionCode,
|
||||
isRequirementActionNeedProject,
|
||||
isRequirementActionNeedReviewChoice,
|
||||
isRequirementActionTerminal
|
||||
isRequirementActionNeedReviewChoice
|
||||
} from '../shared/requirement-master-data';
|
||||
|
||||
defineOptions({ name: 'RequirementActionDialog' });
|
||||
@@ -45,7 +44,7 @@ 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 needReason = computed(() => Boolean(props.action?.needReason));
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (!props.action) return '';
|
||||
@@ -55,7 +54,7 @@ const dialogTitle = computed(() => {
|
||||
|
||||
const reviewChoiceOptions = [
|
||||
{ label: '需要评审', value: 'claim_to_review', description: '认领后进入评审流程' },
|
||||
{ label: '不需要评审', value: 'claim_to_dispatch', description: '认领后直接进入分流' }
|
||||
{ label: '不需要评审', value: 'claim_to_dispatch', description: '认领后直接进入指派' }
|
||||
];
|
||||
|
||||
const rules = computed(() => {
|
||||
@@ -69,7 +68,7 @@ const rules = computed(() => {
|
||||
baseRules.implementProjectId = [createRequiredRule('请选择关联项目')];
|
||||
}
|
||||
|
||||
if (isTerminalAction.value) {
|
||||
if (needReason.value) {
|
||||
baseRules.reason = [createRequiredRule('请输入状态变更原因')];
|
||||
}
|
||||
|
||||
@@ -98,7 +97,7 @@ async function handleSubmit() {
|
||||
payload.implementProjectId = model.value.implementProjectId;
|
||||
}
|
||||
|
||||
if (isTerminalAction.value) {
|
||||
if (needReason.value) {
|
||||
payload.reason = model.value.reason.trim();
|
||||
}
|
||||
|
||||
@@ -142,7 +141,7 @@ async function handleSubmit() {
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="isTerminalAction" label="变更原因" prop="reason">
|
||||
<ElFormItem v-if="needReason" label="变更原因" prop="reason">
|
||||
<ElInput
|
||||
v-model="model.reason"
|
||||
type="textarea"
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<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 { fetchCreateRequirement, fetchGetRequirementModuleTree, fetchGetUserSimpleList } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
import RequirementTreePicker, {
|
||||
type RequirementTreePickerNode
|
||||
} from '@/views/project/project/execution/components/requirement-tree-picker.vue';
|
||||
import MemberSelectOption from './member-select-option.vue';
|
||||
|
||||
defineOptions({ name: 'RequirementCreateDialog' });
|
||||
@@ -36,18 +37,10 @@ const visible = defineModel<boolean>('visible', {
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
const { enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
|
||||
|
||||
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
||||
const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null>(null);
|
||||
|
||||
const priorityOptions = computed(() => {
|
||||
return priorityDictData.value.map(item => ({
|
||||
label: item.label,
|
||||
value: Number(item.value)
|
||||
}));
|
||||
});
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
@@ -78,9 +71,9 @@ interface Model {
|
||||
description: string | null;
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
reviewRequired: number;
|
||||
moduleId: string;
|
||||
moduleId: string | null;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
priority: string | null;
|
||||
expectedTime: string | null;
|
||||
proposerId: string;
|
||||
currentHandlerUserId: string;
|
||||
@@ -90,34 +83,14 @@ interface Model {
|
||||
const submitting = ref(false);
|
||||
const loading = ref(false);
|
||||
const moduleTree = ref<Api.Product.RequirementModule[]>([]);
|
||||
const allUserOptions = ref<Api.SystemManage.UserSimple[]>([]);
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const memberUserOptions = computed(() => {
|
||||
return props.memberOptions.filter(m => m.status === 0);
|
||||
});
|
||||
|
||||
const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() => {
|
||||
const options: Array<{ label: string; value: string }> = [];
|
||||
|
||||
function walk(modules: Api.Product.RequirementModule[], parentPath: string) {
|
||||
for (const module of modules) {
|
||||
const currentPath = `${parentPath}/${module.moduleName}`;
|
||||
options.push({
|
||||
label: currentPath,
|
||||
value: module.id || ''
|
||||
});
|
||||
if (module.children?.length) {
|
||||
walk(module.children, currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleTree.value.length > 0) {
|
||||
walk(moduleTree.value, '');
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
const moduleTreeOptions = computed<RequirementTreePickerNode[]>(() => mapModuleTree(moduleTree.value));
|
||||
|
||||
const rules = {
|
||||
title: [createRequiredRule('请输入需求名称')],
|
||||
@@ -163,9 +136,9 @@ function createDefaultModel(): Model {
|
||||
description: null,
|
||||
attachments: [],
|
||||
reviewRequired: 0,
|
||||
moduleId: props.defaultModuleId || '0',
|
||||
moduleId: props.defaultModuleId || null,
|
||||
category: '功能需求',
|
||||
priority: 1,
|
||||
priority: '3',
|
||||
expectedTime: null,
|
||||
proposerId: '',
|
||||
currentHandlerUserId: '',
|
||||
@@ -173,6 +146,16 @@ function createDefaultModel(): Model {
|
||||
};
|
||||
}
|
||||
|
||||
function mapModuleTree(modules: Api.Product.RequirementModule[]): RequirementTreePickerNode[] {
|
||||
return modules
|
||||
.filter(item => Boolean(item.id))
|
||||
.map(item => ({
|
||||
id: item.id || '',
|
||||
title: item.moduleName,
|
||||
children: item.children?.length ? mapModuleTree(item.children) : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visible.value = false;
|
||||
}
|
||||
@@ -189,8 +172,8 @@ async function handleSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
const proposer = memberUserOptions.value.find(m => m.userId === model.value.proposerId);
|
||||
const proposerNickname = proposer?.userNickname || '';
|
||||
const proposer = allUserOptions.value.find(u => u.id === model.value.proposerId);
|
||||
const proposerNickname = proposer?.nickname || '';
|
||||
const handler = memberUserOptions.value.find(m => m.userId === model.value.currentHandlerUserId);
|
||||
const currentHandlerUserNickname = handler?.userNickname || '';
|
||||
|
||||
@@ -249,6 +232,13 @@ async function loadModuleTree() {
|
||||
moduleTree.value = data;
|
||||
}
|
||||
|
||||
async function loadAllUsers() {
|
||||
const { error, data } = await fetchGetUserSimpleList();
|
||||
if (!error && data) {
|
||||
allUserOptions.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
@@ -257,7 +247,7 @@ watch(
|
||||
}
|
||||
|
||||
model.value = createDefaultModel();
|
||||
await loadModuleTree();
|
||||
await Promise.all([loadModuleTree(), loadAllUsers()]);
|
||||
|
||||
await nextTick();
|
||||
attachmentUploaderRef.value?.initSession();
|
||||
@@ -286,9 +276,11 @@ watch(
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="模块">
|
||||
<ElSelect v-model="model.moduleId" class="w-full" filterable placeholder="请选择所属模块">
|
||||
<ElOption v-for="item in flatModuleOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</ElSelect>
|
||||
<RequirementTreePicker
|
||||
v-model="model.moduleId"
|
||||
:data="moduleTreeOptions"
|
||||
placeholder="搜索或选择所属模块"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否需要评审">
|
||||
@@ -306,9 +298,12 @@ watch(
|
||||
</ElFormItem>
|
||||
|
||||
<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>
|
||||
<DictSelect
|
||||
v-model="model.priority"
|
||||
:dict-code="priorityDictCode"
|
||||
placeholder="请选择优先级"
|
||||
show-remark
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="需求类型" prop="category">
|
||||
@@ -322,14 +317,7 @@ watch(
|
||||
|
||||
<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>
|
||||
<ElOption v-for="item in allUserOptions" :key="item.id" :label="item.nickname" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
fetchGetProjectListByProductId,
|
||||
fetchGetRequirement,
|
||||
fetchGetRequirementModuleTree,
|
||||
fetchGetUserSimpleList,
|
||||
fetchUpdateRequirement
|
||||
} from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
@@ -14,7 +15,11 @@ import BusinessAttachmentUploader from '@/components/custom/business-attachment-
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
import ReadonlyField from '@/components/custom/readonly-field.vue';
|
||||
import RequirementTreePicker, {
|
||||
type RequirementTreePickerNode
|
||||
} from '@/views/project/project/execution/components/requirement-tree-picker.vue';
|
||||
import MemberSelectOption from './member-select-option.vue';
|
||||
|
||||
defineOptions({ name: 'RequirementDetailDialog' });
|
||||
@@ -46,26 +51,19 @@ const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const { getLabel: getCategoryLabel } = useDict(() => props.categoryDictCode);
|
||||
const { getLabel: getPriorityLabel, enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
|
||||
const { getLabel: getPriorityLabel } = useDict(() => props.priorityDictCode);
|
||||
|
||||
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
||||
const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null>(null);
|
||||
|
||||
const priorityOptions = computed(() => {
|
||||
return priorityDictData.value.map(item => ({
|
||||
label: item.label,
|
||||
value: Number(item.value)
|
||||
}));
|
||||
});
|
||||
|
||||
interface Model {
|
||||
title: string;
|
||||
description: string | null;
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
reviewRequired: number;
|
||||
moduleId: string;
|
||||
moduleId: string | null;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
priority: string | null;
|
||||
expectedTime: string | null;
|
||||
proposerId: string;
|
||||
proposerNickname: string;
|
||||
@@ -80,6 +78,7 @@ const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const moduleTree = ref<Api.Product.RequirementModule[]>([]);
|
||||
const projectOptions = ref<Api.Project.Project[]>([]);
|
||||
const allUserOptions = ref<Api.SystemManage.UserSimple[]>([]);
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const isViewMode = computed(() => props.mode === 'view');
|
||||
@@ -100,6 +99,10 @@ const memberLabelMap = computed(() => {
|
||||
return new Map(memberUserOptions.value.map(item => [String(item.userId), item.userNickname]));
|
||||
});
|
||||
|
||||
const allUserLabelMap = computed(() => {
|
||||
return new Map(allUserOptions.value.map(item => [String(item.id), item.nickname]));
|
||||
});
|
||||
|
||||
const moduleLabelMap = computed(() => {
|
||||
const map = new Map<string | undefined, string>();
|
||||
|
||||
@@ -120,28 +123,7 @@ const projectOptionsMap = computed(() => {
|
||||
return new Map(projectOptions.value.map(item => [String(item.id), item.projectName]));
|
||||
});
|
||||
|
||||
const flatModuleOptions = computed<Array<{ label: string; value: string }>>(() => {
|
||||
const options: Array<{ label: string; value: string }> = [];
|
||||
|
||||
function walk(modules: Api.Product.RequirementModule[], parentPath: string) {
|
||||
for (const module of modules) {
|
||||
const currentPath = `${parentPath}/${module.moduleName}`;
|
||||
options.push({
|
||||
label: currentPath,
|
||||
value: module.id || ''
|
||||
});
|
||||
if (module.children?.length) {
|
||||
walk(module.children, currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleTree.value.length > 0) {
|
||||
walk(moduleTree.value, '');
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
const moduleTreeOptions = computed<RequirementTreePickerNode[]>(() => mapModuleTree(moduleTree.value));
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
@@ -216,9 +198,9 @@ function createDefaultModel(): Model {
|
||||
description: null,
|
||||
attachments: [],
|
||||
reviewRequired: 0,
|
||||
moduleId: '0',
|
||||
moduleId: null,
|
||||
category: '',
|
||||
priority: 1,
|
||||
priority: '3',
|
||||
expectedTime: null,
|
||||
proposerId: '',
|
||||
proposerNickname: '',
|
||||
@@ -230,6 +212,16 @@ function createDefaultModel(): Model {
|
||||
};
|
||||
}
|
||||
|
||||
function mapModuleTree(modules: Api.Product.RequirementModule[]): RequirementTreePickerNode[] {
|
||||
return modules
|
||||
.filter(item => Boolean(item.id))
|
||||
.map(item => ({
|
||||
id: item.id || '',
|
||||
title: item.moduleName,
|
||||
children: item.children?.length ? mapModuleTree(item.children) : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visible.value = false;
|
||||
}
|
||||
@@ -322,9 +314,9 @@ function transformRequirementData(data: Api.Product.Requirement): typeof model.v
|
||||
description: data.description || null,
|
||||
attachments: data.attachments ? [...data.attachments] : [],
|
||||
reviewRequired: data.reviewRequired ?? 0,
|
||||
moduleId: data.moduleId || '0',
|
||||
moduleId: data.moduleId || null,
|
||||
category: data.category || '',
|
||||
priority: data.priority ?? null,
|
||||
priority: data.priority === null || data.priority === undefined ? null : String(data.priority),
|
||||
expectedTime: formatExpectedTime(data.expectedTime),
|
||||
proposerId: data.proposerId || '',
|
||||
proposerNickname: data.proposerNickname || '',
|
||||
@@ -367,6 +359,13 @@ async function loadRequirementDetail() {
|
||||
model.value = transformRequirementData(data);
|
||||
}
|
||||
|
||||
async function loadAllUsers() {
|
||||
const { error, data } = await fetchGetUserSimpleList();
|
||||
if (!error && data) {
|
||||
allUserOptions.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
@@ -374,7 +373,7 @@ watch(
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([loadModuleTree(), loadProjectOptions()]);
|
||||
await Promise.all([loadModuleTree(), loadProjectOptions(), loadAllUsers()]);
|
||||
|
||||
if (props.requirement?.id) {
|
||||
await loadRequirementDetail();
|
||||
@@ -414,11 +413,14 @@ watch(
|
||||
|
||||
<ElFormItem label="模块">
|
||||
<template v-if="isViewMode">
|
||||
<ReadonlyField :value="moduleLabelMap.get(model.moduleId) || '--'" />
|
||||
<ReadonlyField :value="moduleLabelMap.get(model.moduleId || undefined) || '--'" />
|
||||
</template>
|
||||
<ElSelect v-else v-model="model.moduleId" class="w-full" filterable placeholder="请选择所属模块">
|
||||
<ElOption v-for="item in flatModuleOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</ElSelect>
|
||||
<RequirementTreePicker
|
||||
v-else
|
||||
v-model="model.moduleId"
|
||||
:data="moduleTreeOptions"
|
||||
placeholder="搜索或选择所属模块"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否需要评审">
|
||||
@@ -431,9 +433,13 @@ watch(
|
||||
: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>
|
||||
<DictSelect
|
||||
v-else
|
||||
v-model="model.priority"
|
||||
:dict-code="priorityDictCode"
|
||||
placeholder="请选择优先级"
|
||||
show-remark
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="需求类型" prop="category">
|
||||
@@ -441,7 +447,7 @@ watch(
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="提出人" prop="proposerId">
|
||||
<ReadonlyField :value="memberLabelMap.get(model.proposerId) || '--'" />
|
||||
<ReadonlyField :value="allUserLabelMap.get(model.proposerId) || '--'" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||
@@ -512,7 +518,7 @@ watch(
|
||||
:disabled="isViewMode"
|
||||
:height="editorHeight"
|
||||
upload-directory="requirement"
|
||||
placeholder="请输入需求内容"
|
||||
:placeholder="isViewMode && isEmptyRichText(model.description) ? '--' : '请输入需求内容'"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchSubmitProductRequirementReview } from '@/service/api';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||
import ReadonlyField from '@/components/custom/readonly-field.vue';
|
||||
import AttendeeUserPicker from '@/components/custom/attendee-user-picker.vue';
|
||||
|
||||
defineOptions({ name: 'RequirementReviewDialog' });
|
||||
|
||||
interface Props {
|
||||
productId: string;
|
||||
requirement: Api.Product.Requirement | null;
|
||||
memberOptions: Api.Product.ProductMember[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
||||
const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null>(null);
|
||||
|
||||
interface Model {
|
||||
conclusion: Api.Product.RequirementReviewConclusion;
|
||||
attendees: Api.Product.RequirementReviewAttendeeItem[];
|
||||
requirementEstimatedHours: number | null;
|
||||
reviewTime: string | null;
|
||||
reviewContent: string | null;
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
}
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
const submitting = ref(false);
|
||||
|
||||
const memberUserOptions = computed(() => props.memberOptions.filter(item => item.status === 0));
|
||||
|
||||
const reviewConclusionOptions = [
|
||||
{ label: '通过评审', value: 0 as Api.Product.RequirementReviewConclusion },
|
||||
{ label: '不通过评审', value: 1 as Api.Product.RequirementReviewConclusion }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
conclusion: [createRequiredRule('请选择评审结论')],
|
||||
attendees: [createRequiredRule('请选择参会人')]
|
||||
} satisfies Record<string, App.Global.FormRule[]>;
|
||||
|
||||
const leftColRef = ref<HTMLElement>();
|
||||
const editorHeight = ref<string>('45vh');
|
||||
|
||||
const ATTACHMENT_SECTION_RESERVE_PX = 140;
|
||||
|
||||
useResizeObserver(leftColRef, entries => {
|
||||
const height = entries[0]?.contentRect.height;
|
||||
|
||||
if (height && height > 120) {
|
||||
editorHeight.value = `${Math.max(height - 60 - ATTACHMENT_SECTION_RESERVE_PX, 200)}px`;
|
||||
}
|
||||
});
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
conclusion: 0,
|
||||
attendees: [],
|
||||
requirementEstimatedHours: null,
|
||||
reviewTime: dayjs().format('YYYY-MM-DD'),
|
||||
reviewContent: null,
|
||||
attachments: []
|
||||
};
|
||||
}
|
||||
|
||||
function isEmptyRichText(html: string | null | undefined) {
|
||||
if (!html) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const text = html
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /g, '')
|
||||
.trim();
|
||||
|
||||
if (text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !/<img\b/i.test(html);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
if (!props.productId || !props.requirement?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authStore.userInfo.userId) {
|
||||
window.$message?.warning('未获取到当前登录用户信息');
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachmentUploaderRef.value?.hasUploading) {
|
||||
window.$message?.warning('附件正在上传中,请稍候');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Api.Product.RequirementReviewSubmitParams = {
|
||||
productId: props.productId,
|
||||
requirementId: props.requirement.id,
|
||||
operatorId: authStore.userInfo.userId,
|
||||
conclusion: model.value.conclusion,
|
||||
reviewContent: isEmptyRichText(model.value.reviewContent) ? null : (model.value.reviewContent ?? null),
|
||||
requirementEstimatedHours: model.value.requirementEstimatedHours,
|
||||
attendees: [...model.value.attendees],
|
||||
attachments: [...model.value.attachments],
|
||||
reviewTime: model.value.reviewTime
|
||||
};
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const result = await fetchSubmitProductRequirementReview(payload);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]);
|
||||
|
||||
window.$message?.success('评审提交成功');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
async value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.value = createDefaultModel();
|
||||
|
||||
await nextTick();
|
||||
attachmentUploaderRef.value?.initSession();
|
||||
richTextEditorRef.value?.initSession();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="评审需求"
|
||||
width="1100px"
|
||||
max-body-height="78vh"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<div class="requirement-review-dialog__grid">
|
||||
<div ref="leftColRef" class="requirement-review-dialog__col-left">
|
||||
<BusinessFormSection title="评审信息">
|
||||
<ElFormItem label="需求名称">
|
||||
<ReadonlyField :value="requirement?.title || '--'" />
|
||||
<!-- <ElInput :model-value="requirement?.title || ''" readonly placeholder="--" />-->
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="评审结论" prop="conclusion">
|
||||
<ElRadioGroup v-model="model.conclusion">
|
||||
<ElRadio
|
||||
v-for="item in reviewConclusionOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
border
|
||||
style="width: 165px"
|
||||
>
|
||||
{{ item.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="参会人" prop="attendees">
|
||||
<AttendeeUserPicker
|
||||
v-model="model.attendees"
|
||||
:team-options="memberUserOptions"
|
||||
team-tab-label="产品团队"
|
||||
:show-dept-tab="true"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="需求预估工时">
|
||||
<ElInputNumber
|
||||
v-model="model.requirementEstimatedHours"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:step="0.5"
|
||||
:precision="1"
|
||||
placeholder="请输入需求预估工时"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="实际评审日期">
|
||||
<ElDatePicker
|
||||
v-model="model.reviewTime"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择实际评审日期"
|
||||
class="requirement-review-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
|
||||
<div class="requirement-review-dialog__col-right">
|
||||
<BusinessFormSection title="评审内容">
|
||||
<ElFormItem class="requirement-review-dialog__desc-item">
|
||||
<BusinessRichTextEditor
|
||||
ref="richTextEditorRef"
|
||||
v-model="model.reviewContent"
|
||||
:height="editorHeight"
|
||||
upload-directory="requirement-review"
|
||||
placeholder="请输入评审内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="会议资料">
|
||||
<ElFormItem class="requirement-review-dialog__attachment-item">
|
||||
<BusinessAttachmentUploader
|
||||
ref="attachmentUploaderRef"
|
||||
v-model="model.attachments"
|
||||
directory="requirement-review"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.requirement-review-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.requirement-review-dialog__col-left,
|
||||
.requirement-review-dialog__col-right {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.requirement-review-dialog__col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.requirement-review-dialog__desc-item,
|
||||
.requirement-review-dialog__attachment-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (width <= 1024px) {
|
||||
.requirement-review-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.requirement-review-dialog__date-picker.el-date-editor.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,312 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchGetProductRequirementReview } from '@/service/api';
|
||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||
import ReadonlyField from '@/components/custom/readonly-field.vue';
|
||||
|
||||
defineOptions({ name: 'RequirementReviewRecordDialog' });
|
||||
|
||||
interface Props {
|
||||
productId: string;
|
||||
requirement: Api.Product.Requirement | null;
|
||||
memberOptions: Api.Product.ProductMember[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const reviewRecord = ref<Api.Product.RequirementReview | null>(null);
|
||||
const leftColRef = ref<HTMLElement>();
|
||||
const editorHeight = ref<string>('47vh');
|
||||
|
||||
const ATTACHMENT_SECTION_RESERVE_PX = 140;
|
||||
const ATTENDEE_VISIBLE_COUNT = 5;
|
||||
|
||||
useResizeObserver(leftColRef, entries => {
|
||||
const height = entries[0]?.contentRect.height;
|
||||
|
||||
if (height && height > 120) {
|
||||
editorHeight.value = `${Math.max(height - 60 - ATTACHMENT_SECTION_RESERVE_PX, 200)}px`;
|
||||
}
|
||||
});
|
||||
|
||||
const operatorLabelMap = computed(() => {
|
||||
return new Map(props.memberOptions.map(item => [item.userId, item.userNickname]));
|
||||
});
|
||||
|
||||
const conclusionText = computed(() => {
|
||||
if (!reviewRecord.value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return reviewRecord.value.conclusion === 0 ? '通过评审' : '不通过评审';
|
||||
});
|
||||
|
||||
const operatorText = computed(() => {
|
||||
if (!reviewRecord.value?.operatorId) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return operatorLabelMap.value.get(reviewRecord.value.operatorId) || reviewRecord.value.operatorId;
|
||||
});
|
||||
|
||||
const visibleAttendees = computed(() => reviewRecord.value?.attendees?.slice(0, ATTENDEE_VISIBLE_COUNT) ?? []);
|
||||
const overflowAttendees = computed(() => reviewRecord.value?.attendees?.slice(ATTENDEE_VISIBLE_COUNT) ?? []);
|
||||
|
||||
function formatExpectedTime(value?: string | number[] | null): string {
|
||||
if (!value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string | null): string {
|
||||
if (!value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
async function loadReviewRecord() {
|
||||
if (!props.productId || !props.requirement?.id) {
|
||||
reviewRecord.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const { error, data } = await fetchGetProductRequirementReview(props.productId, props.requirement.id);
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (error || !data) {
|
||||
reviewRecord.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
reviewRecord.value = data;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
loadReviewRecord();
|
||||
} else {
|
||||
reviewRecord.value = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="查看评审记录"
|
||||
width="1100px"
|
||||
max-body-height="78vh"
|
||||
:loading="loading"
|
||||
:show-footer="true"
|
||||
>
|
||||
<template #footer="{ close }">
|
||||
<ElButton type="primary" @click="close">关闭</ElButton>
|
||||
</template>
|
||||
|
||||
<ElEmpty v-if="!loading && !reviewRecord" description="暂无评审记录" />
|
||||
|
||||
<div v-else class="requirement-review-record-dialog__grid">
|
||||
<div ref="leftColRef" class="requirement-review-record-dialog__col-left">
|
||||
<BusinessFormSection title="评审信息">
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem label="需求名称">
|
||||
<ReadonlyField :value="requirement?.title || '--'" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="评审结论">
|
||||
<ReadonlyField :value="conclusionText" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="评审提交人">
|
||||
<ReadonlyField :value="operatorText" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="参会人">
|
||||
<div v-if="reviewRecord?.attendees?.length" class="requirement-review-record-dialog__tags">
|
||||
<ElTag v-for="item in visibleAttendees" :key="item.userId" effect="light">
|
||||
{{ item.nickname }}
|
||||
</ElTag>
|
||||
<ElPopover
|
||||
v-if="overflowAttendees.length"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
:width="280"
|
||||
popper-class="requirement-review-record-dialog__attendee-popper"
|
||||
>
|
||||
<template #reference>
|
||||
<button type="button" class="requirement-review-record-dialog__tag-more">
|
||||
+{{ overflowAttendees.length }} 更多
|
||||
</button>
|
||||
</template>
|
||||
<div class="requirement-review-record-dialog__attendee-overflow">
|
||||
<div class="requirement-review-record-dialog__attendee-overflow-head">
|
||||
另外
|
||||
<strong>{{ overflowAttendees.length }}</strong>
|
||||
人
|
||||
</div>
|
||||
<div class="requirement-review-record-dialog__attendee-overflow-tags">
|
||||
<ElTag v-for="item in overflowAttendees" :key="item.userId" effect="light">
|
||||
{{ item.nickname }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
</ElPopover>
|
||||
</div>
|
||||
<ReadonlyField v-else value="--" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="需求预估工时">
|
||||
<ReadonlyField
|
||||
:value="
|
||||
reviewRecord?.requirementEstimatedHours !== null &&
|
||||
reviewRecord?.requirementEstimatedHours !== undefined &&
|
||||
reviewRecord?.requirementEstimatedHours !== ''
|
||||
? String(reviewRecord.requirementEstimatedHours)
|
||||
: '--'
|
||||
"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="实际评审日期">
|
||||
<ReadonlyField :value="formatExpectedTime(reviewRecord?.reviewTime)" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="提交时间">
|
||||
<ReadonlyField :value="formatDateTime(reviewRecord?.createTime)" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
|
||||
<div class="requirement-review-record-dialog__col-right">
|
||||
<BusinessFormSection title="评审内容">
|
||||
<ElFormItem class="requirement-review-record-dialog__desc-item">
|
||||
<BusinessRichTextEditor
|
||||
:model-value="reviewRecord?.reviewContent || ''"
|
||||
disabled
|
||||
:height="editorHeight"
|
||||
upload-directory="requirement-review"
|
||||
placeholder="--"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="会议资料">
|
||||
<ElFormItem class="requirement-review-record-dialog__attachment-item">
|
||||
<BusinessAttachmentUploader
|
||||
:model-value="reviewRecord?.attachments || []"
|
||||
disabled
|
||||
directory="requirement-review"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
</div>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.requirement-review-record-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__col-left,
|
||||
.requirement-review-record-dialog__col-right {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__desc-item,
|
||||
.requirement-review-record-dialog__attachment-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__tag-more {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
font-size: 11.5px;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__tag-more:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__attendee-overflow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__attendee-overflow-head {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__attendee-overflow-head strong {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.requirement-review-record-dialog__attendee-overflow-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (width <= 1024px) {
|
||||
.requirement-review-record-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -124,7 +124,7 @@ const fields = computed(() => [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableSearchFields v-model="model" :fields="fields" :columns="3" @search="emit('search')" @reset="emit('reset')" />
|
||||
<TableSearchFields v-model="model" :fields="fields" :columns="4" @search="emit('search')" @reset="emit('reset')" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -4,11 +4,11 @@ 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';
|
||||
import BusinessAttachmentUploader from '@/components/custom/business-attachment-uploader.vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessFormSection from '@/components/custom/business-form-section.vue';
|
||||
import BusinessRichTextEditor from '@/components/custom/business-rich-text-editor.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
import MemberSelectOption from './member-select-option.vue';
|
||||
|
||||
defineOptions({ name: 'RequirementSplitDialog' });
|
||||
@@ -35,18 +35,10 @@ const visible = defineModel<boolean>('visible', {
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
const { enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
|
||||
|
||||
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
||||
const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null>(null);
|
||||
|
||||
const priorityOptions = computed(() => {
|
||||
return priorityDictData.value.map(item => ({
|
||||
label: item.label,
|
||||
value: Number(item.value)
|
||||
}));
|
||||
});
|
||||
|
||||
const reviewRequiredOptions = [
|
||||
{ label: '不需要', value: 0 },
|
||||
{ label: '需要', value: 1 }
|
||||
@@ -78,7 +70,7 @@ interface Model {
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
reviewRequired: number;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
priority: string | null;
|
||||
expectedTime: string | null;
|
||||
currentHandlerUserId: string;
|
||||
sort: number;
|
||||
@@ -135,7 +127,7 @@ function createDefaultModel(): Model {
|
||||
attachments: [],
|
||||
reviewRequired: 0,
|
||||
category: '',
|
||||
priority: 1,
|
||||
priority: '3',
|
||||
expectedTime: null,
|
||||
currentHandlerUserId: '',
|
||||
sort: 0
|
||||
@@ -265,9 +257,12 @@ watch(
|
||||
</ElFormItem>
|
||||
|
||||
<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>
|
||||
<DictSelect
|
||||
v-model="model.priority"
|
||||
:dict-code="priorityDictCode"
|
||||
placeholder="请选择优先级"
|
||||
show-remark
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||
|
||||
Reference in New Issue
Block a user