diff --git a/src/service/api/product.ts b/src/service/api/product.ts index 62c95f1..18dd8db 100644 --- a/src/service/api/product.ts +++ b/src/service/api/product.ts @@ -183,7 +183,14 @@ const REQUIREMENT_PREFIX = `${WEB_SERVICE_PREFIX}/project/product/requirement`; type RequirementResponse = Omit< Api.Product.Requirement, - 'id' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'implementProjectId' | 'sourceBizId' + | 'id' + | 'parentId' + | 'moduleId' + | 'proposerId' + | 'currentHandlerUserId' + | 'implementProjectId' + | 'sourceBizId' + | 'attachments' > & { id: string | number; parentId: string | number; @@ -193,11 +200,32 @@ type RequirementResponse = Omit< implementProjectId?: string | number | null; implementProjectName?: string | null; sourceBizId?: string | number | null; + attachments?: AttachmentItemResponse[] | null; children?: RequirementResponse[]; }; type RequirementPageResponse = Api.Product.PageResult; +type AttachmentItemResponse = Omit & { + fileId?: string | number; + id?: string | number; +}; + +function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null { + if (!list) { + return null; + } + + return list.map(item => { + const rawId = item.fileId ?? item.id; + + return { + ...item, + fileId: rawId === null || rawId === undefined ? '' : String(rawId) + }; + }); +} + function normalizeRequirement(requirement: RequirementResponse): Api.Product.Requirement { return { ...requirement, @@ -209,6 +237,7 @@ function normalizeRequirement(requirement: RequirementResponse): Api.Product.Req implementProjectId: normalizeNullableStringId(requirement.implementProjectId), implementProjectName: requirement.implementProjectName ?? null, sourceBizId: normalizeNullableStringId(requirement.sourceBizId), + attachments: normalizeAttachments(requirement.attachments), children: requirement.children?.map(normalizeRequirement) }; } diff --git a/src/service/api/project.ts b/src/service/api/project.ts index ac6d894..721120b 100644 --- a/src/service/api/project.ts +++ b/src/service/api/project.ts @@ -744,7 +744,7 @@ const PROJECT_REQUIREMENT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project/requir type ProjectRequirementResponse = Omit< Api.Project.ProjectRequirement, - 'id' | 'projectId' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'sourceBizId' + 'id' | 'projectId' | 'parentId' | 'moduleId' | 'proposerId' | 'currentHandlerUserId' | 'sourceBizId' | 'attachments' > & { id: string | number; projectId: string | number; @@ -753,6 +753,7 @@ type ProjectRequirementResponse = Omit< proposerId: string | number; currentHandlerUserId?: string | number | null; sourceBizId?: string | number | null; + attachments?: AttachmentItemResponse[] | null; children?: ProjectRequirementResponse[]; }; @@ -765,6 +766,26 @@ type ProjectRequirementModuleResponse = Omit & { + fileId?: string | number; + id?: string | number; +}; + +function normalizeAttachments(list?: AttachmentItemResponse[] | null): Api.Project.AttachmentItem[] | null { + if (!list) { + return null; + } + + return list.map(item => { + const rawId = item.fileId ?? item.id; + + return { + ...item, + fileId: rawId === null || rawId === undefined ? '' : String(rawId) + }; + }); +} + function normalizeProjectRequirement(requirement: ProjectRequirementResponse): Api.Project.ProjectRequirement { return { ...requirement, @@ -775,6 +796,7 @@ function normalizeProjectRequirement(requirement: ProjectRequirementResponse): A proposerId: normalizeStringId(requirement.proposerId), currentHandlerUserId: normalizeNullableStringId(requirement.currentHandlerUserId), sourceBizId: normalizeNullableStringId(requirement.sourceBizId), + attachments: normalizeAttachments(requirement.attachments), children: requirement.children?.map(normalizeProjectRequirement) }; } diff --git a/src/typings/api/product.d.ts b/src/typings/api/product.d.ts index 2edd3c9..bc7fef8 100644 --- a/src/typings/api/product.d.ts +++ b/src/typings/api/product.d.ts @@ -270,6 +270,8 @@ declare namespace Api { title: string; /** 需求内容(富文本) */ description?: string | null; + /** 附件列表 */ + attachments?: Api.Project.AttachmentItem[] | null; /** 需求类型字典值 */ category: string; /** 需求类型名称 */ @@ -391,6 +393,7 @@ declare namespace Api { | 'reviewRequired' | 'title' | 'description' + | 'attachments' | 'category' | 'priority' | 'proposerId' @@ -430,6 +433,7 @@ declare namespace Api { | 'reviewRequired' | 'title' | 'description' + | 'attachments' | 'category' | 'priority' | 'proposerId' diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index 2389b40..daf3fa2 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -635,6 +635,8 @@ declare namespace Api { title: string; /** 需求描述 */ description?: string | null; + /** 附件列表 */ + attachments?: AttachmentItem[] | null; /** 需求分类字典值 */ category: string; /** 需求分类名称 */ @@ -744,6 +746,7 @@ declare namespace Api { | 'reviewRequired' | 'title' | 'description' + | 'attachments' | 'category' | 'priority' | 'proposerId' @@ -781,6 +784,7 @@ declare namespace Api { | 'reviewRequired' | 'title' | 'description' + | 'attachments' | 'category' | 'priority' | 'proposerId' diff --git a/src/views/product/requirement/modules/module-tree-node.vue b/src/views/product/requirement/modules/module-tree-node.vue index 25f3e13..541b6b4 100644 --- a/src/views/product/requirement/modules/module-tree-node.vue +++ b/src/views/product/requirement/modules/module-tree-node.vue @@ -125,7 +125,9 @@ function handleToggle() {
- {{ module.moduleName }} + + {{ module.moduleName }} + import { computed, nextTick, ref, watch } from 'vue'; +import { useResizeObserver } from '@vueuse/core'; import { fetchCreateRequirement, fetchGetRequirementModuleTree } 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'; @@ -32,9 +35,11 @@ const visible = defineModel('visible', { const { formRef, validate } = useForm(); const { createRequiredRule } = useFormRules(); - const { enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode); +const attachmentUploaderRef = ref | null>(null); +const richTextEditorRef = ref | null>(null); + const priorityOptions = computed(() => { return priorityDictData.value.map(item => ({ label: item.label, @@ -44,7 +49,8 @@ const priorityOptions = computed(() => { interface Model { title: string; - description: string; + description: string | null; + attachments: Api.Project.AttachmentItem[]; reviewRequired: number; moduleId: string; category: string; @@ -58,7 +64,6 @@ interface Model { const submitting = ref(false); const loading = ref(false); const moduleTree = ref([]); - const model = ref(createDefaultModel()); const memberUserOptions = computed(() => { @@ -102,10 +107,41 @@ const rules = { workHours: [createRequiredRule('请输入所需工时')] } satisfies Record; +const leftColRef = ref(); +const editorHeight = ref('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 isEmptyRichText(html: string | null | undefined) { + if (!html) { + return true; + } + + const text = html + .replace(/<[^>]+>/g, '') + .replace(/ /g, '') + .trim(); + + if (text) { + return false; + } + + return !/ m.userId === model.value.proposerId); const proposerNickname = proposer?.userNickname || ''; const handler = memberUserOptions.value.find(m => m.userId === model.value.currentHandlerUserId); @@ -142,7 +179,8 @@ async function handleSubmit() { moduleId: model.value.moduleId || '0', reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired, title: model.value.title.trim(), - description: getNullableText(model.value.description), + description: isEmptyRichText(model.value.description) ? null : (model.value.description ?? null), + attachments: [...model.value.attachments], category: model.value.category, priority: Number(model.value.priority) as Api.Product.RequirementPriority, proposerId: model.value.proposerId, @@ -164,6 +202,8 @@ async function handleSubmit() { return; } + await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]); + window.$message?.success('需求新增成功'); closeDialog(); emit('submitted'); @@ -175,8 +215,12 @@ async function loadModuleTree() { return; } + loading.value = true; + const { error, data } = await fetchGetRequirementModuleTree(props.productId); + loading.value = false; + if (error || !data) { moduleTree.value = []; return; @@ -196,6 +240,8 @@ watch( await loadModuleTree(); await nextTick(); + attachmentUploaderRef.value?.initSession(); + richTextEditorRef.value?.initSession(); formRef.value?.clearValidate(); } ); @@ -205,112 +251,150 @@ watch( - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+
- + diff --git a/src/views/product/requirement/modules/requirement-detail-dialog.vue b/src/views/product/requirement/modules/requirement-detail-dialog.vue index 9d05af5..2d81b0e 100644 --- a/src/views/product/requirement/modules/requirement-detail-dialog.vue +++ b/src/views/product/requirement/modules/requirement-detail-dialog.vue @@ -1,5 +1,6 @@ + diff --git a/src/views/project/project/requirement/modules/module-tree-node.vue b/src/views/project/project/requirement/modules/module-tree-node.vue index 64a979d..8b2e6ef 100644 --- a/src/views/project/project/requirement/modules/module-tree-node.vue +++ b/src/views/project/project/requirement/modules/module-tree-node.vue @@ -108,7 +108,9 @@ function handleToggle() {
- {{ module.moduleName }} + + {{ module.moduleName }} + import { computed, nextTick, ref, watch } from 'vue'; +import { useResizeObserver } from '@vueuse/core'; import { fetchCreateProjectRequirement, fetchGetProjectRequirementModuleTree } 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'; @@ -34,6 +37,9 @@ const { formRef, validate } = useForm(); const { createRequiredRule } = useFormRules(); const { enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode); +const attachmentUploaderRef = ref | null>(null); +const richTextEditorRef = ref | null>(null); + const priorityOptions = computed(() => { return priorityDictData.value.map(item => ({ label: item.label, @@ -43,7 +49,8 @@ const priorityOptions = computed(() => { interface Model { title: string; - description: string; + description: string | null; + attachments: Api.Project.AttachmentItem[]; reviewRequired: number; moduleId: string; category: string; @@ -55,6 +62,7 @@ interface Model { } const moduleTree = ref([]); +const loading = ref(false); const submitting = ref(false); const model = ref(createDefaultModel()); @@ -98,10 +106,41 @@ const rules = { workHours: [createRequiredRule('请输入所需工时')] } satisfies Record; +const leftColRef = ref(); +const editorHeight = ref('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 isEmptyRichText(html: string | null | undefined) { + if (!html) { + return true; + } + + const text = html + .replace(/<[^>]+>/g, '') + .replace(/ /g, '') + .trim(); + + if (text) { + return false; + } + + return !/ item.userId === model.value.proposerId); const handler = memberUserOptions.value.find(item => item.userId === model.value.currentHandlerUserId); @@ -148,7 +192,8 @@ async function handleSubmit() { moduleId: model.value.moduleId || '0', reviewRequired: model.value.reviewRequired as Api.Project.ProjectRequirementReviewRequired, title: model.value.title.trim(), - description: getNullableText(model.value.description), + description: isEmptyRichText(model.value.description) ? null : (model.value.description ?? null), + attachments: [...model.value.attachments], category: model.value.category, priority: Number(model.value.priority) as Api.Project.ProjectRequirementPriority, proposerId: model.value.proposerId, @@ -169,6 +214,8 @@ async function handleSubmit() { return; } + await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]); + window.$message?.success('项目需求新增成功'); visible.value = false; emit('submitted'); @@ -184,6 +231,8 @@ watch( model.value = createDefaultModel(); await loadModuleTree(); await nextTick(); + attachmentUploaderRef.value?.initSession(); + richTextEditorRef.value?.initSession(); formRef.value?.clearValidate(); } ); @@ -193,111 +242,150 @@ watch( - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+
- + diff --git a/src/views/project/project/requirement/modules/requirement-detail-dialog.vue b/src/views/project/project/requirement/modules/requirement-detail-dialog.vue index 8ea8dca..e5c1363 100644 --- a/src/views/project/project/requirement/modules/requirement-detail-dialog.vue +++ b/src/views/project/project/requirement/modules/requirement-detail-dialog.vue @@ -1,5 +1,6 @@ diff --git a/src/views/project/project/requirement/modules/requirement-split-dialog.vue b/src/views/project/project/requirement/modules/requirement-split-dialog.vue index aa45d45..ad66aa3 100644 --- a/src/views/project/project/requirement/modules/requirement-split-dialog.vue +++ b/src/views/project/project/requirement/modules/requirement-split-dialog.vue @@ -1,9 +1,12 @@