feat(产品需求、项目需求): 开发两种需求的富文本和附件功能。
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
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 MemberSelectOption from './member-select-option.vue';
|
||||
|
||||
@@ -31,9 +34,11 @@ 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,
|
||||
@@ -43,7 +48,8 @@ const priorityOptions = computed(() => {
|
||||
|
||||
interface Model {
|
||||
title: string;
|
||||
description: string;
|
||||
description: string | null;
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
reviewRequired: number;
|
||||
category: string;
|
||||
priority: number | null;
|
||||
@@ -54,7 +60,6 @@ interface Model {
|
||||
|
||||
const submitting = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const memberUserOptions = computed(() => {
|
||||
@@ -73,10 +78,41 @@ const rules = {
|
||||
workHours: [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 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);
|
||||
}
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
description: null,
|
||||
attachments: [],
|
||||
reviewRequired: 0,
|
||||
category: '',
|
||||
priority: 1,
|
||||
@@ -86,10 +122,6 @@ function createDefaultModel(): Model {
|
||||
};
|
||||
}
|
||||
|
||||
function getNullableText(value?: string | null) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
visible.value = false;
|
||||
}
|
||||
@@ -101,18 +133,23 @@ async function handleSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
const proposerNickname = props.parentRequirement.proposerNickname || '';
|
||||
const currentHandlerUserNickname = props.parentRequirement.currentHandlerUserNickname || '';
|
||||
if (attachmentUploaderRef.value?.hasUploading) {
|
||||
window.$message?.warning('附件正在上传中,请稍候');
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = memberUserOptions.value.find(item => item.userId === model.value.currentHandlerUserId);
|
||||
|
||||
const payload: Api.Product.SplitRequirementParams = {
|
||||
parentId: props.parentRequirement.id,
|
||||
productId: props.productId,
|
||||
moduleId: props.parentRequirement.moduleId,
|
||||
proposerId: props.parentRequirement.proposerId,
|
||||
proposerNickname,
|
||||
currentHandlerUserNickname,
|
||||
proposerNickname: props.parentRequirement.proposerNickname || '',
|
||||
currentHandlerUserNickname: handler?.userNickname || '',
|
||||
title: model.value.title.trim(),
|
||||
description: getNullableText(model.value.description),
|
||||
description: isEmptyRichText(model.value.description) ? null : (model.value.description ?? null),
|
||||
attachments: [...model.value.attachments],
|
||||
reviewRequired: model.value.reviewRequired as Api.Product.RequirementReviewRequired,
|
||||
category: model.value.category,
|
||||
priority: Number(model.value.priority) as Api.Product.RequirementPriority,
|
||||
@@ -131,6 +168,8 @@ async function handleSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]);
|
||||
|
||||
window.$message?.success('需求拆分成功');
|
||||
closeDialog();
|
||||
emit('submitted');
|
||||
@@ -149,12 +188,13 @@ watch(
|
||||
model.value.category = props.parentRequirement.category;
|
||||
}
|
||||
|
||||
// 默认选中父需求的负责人
|
||||
if (props.parentRequirement?.currentHandlerUserId) {
|
||||
model.value.currentHandlerUserId = props.parentRequirement.currentHandlerUserId;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
attachmentUploaderRef.value?.initSession();
|
||||
richTextEditorRef.value?.initSession();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
);
|
||||
@@ -164,7 +204,8 @@ watch(
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
title="拆分需求"
|
||||
preset="lg"
|
||||
width="1100px"
|
||||
max-body-height="78vh"
|
||||
:loading="loading"
|
||||
:confirm-loading="submitting"
|
||||
@confirm="handleSubmit"
|
||||
@@ -177,76 +218,116 @@ watch(
|
||||
class="mb-16px"
|
||||
/>
|
||||
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="子需求名称" prop="title">
|
||||
<ElInput v-model="model.title" clearable maxlength="256" placeholder="请输入子需求名称" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="是否需要评审">
|
||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
||||
<ElOption
|
||||
v-for="item in reviewRequiredOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<div class="requirement-operate-dialog__grid">
|
||||
<div ref="leftColRef" class="requirement-operate-dialog__col-left">
|
||||
<BusinessFormSection title="子需求信息">
|
||||
<ElFormItem label="子需求名称" prop="title">
|
||||
<ElInput v-model="model.title" clearable maxlength="256" placeholder="请输入子需求名称" />
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="是否需要评审">
|
||||
<ElSelect v-model="model.reviewRequired" class="w-full" placeholder="请选择">
|
||||
<ElOption
|
||||
v-for="item in reviewRequiredOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="优先级" prop="priority">
|
||||
<ElSelect v-model="model.priority" class="w-full" filterable placeholder="请选择优先级">
|
||||
<ElOption v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="所需工时" prop="workHours">
|
||||
<ElInputNumber
|
||||
v-model="model.workHours"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="1"
|
||||
placeholder="请输入所需工时"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="内容">
|
||||
<BusinessRichTextEditor
|
||||
v-model="model.description"
|
||||
height="240px"
|
||||
upload-directory="requirement"
|
||||
placeholder="请输入需求内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="优先级" prop="priority">
|
||||
<ElSelect v-model="model.priority" class="w-full" filterable placeholder="请选择优先级">
|
||||
<ElOption v-for="item in priorityOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="所需工时" prop="workHours">
|
||||
<ElInputNumber
|
||||
v-model="model.workHours"
|
||||
class="w-full"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
:precision="1"
|
||||
placeholder="请输入所需工时"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
|
||||
<ElOption
|
||||
v-for="item in memberUserOptions"
|
||||
:key="item.userId"
|
||||
:label="item.userNickname"
|
||||
:value="item.userId"
|
||||
>
|
||||
<MemberSelectOption :nickname="item.userNickname" :role-name="item.roleName" />
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="排序值">
|
||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="负责人" prop="currentHandlerUserId">
|
||||
<ElSelect v-model="model.currentHandlerUserId" class="w-full" filterable placeholder="请选择负责人">
|
||||
<ElOption
|
||||
v-for="item in memberUserOptions"
|
||||
:key="item.userId"
|
||||
:label="item.userNickname"
|
||||
:value="item.userId"
|
||||
>
|
||||
<MemberSelectOption :nickname="item.userNickname" :role-name="item.roleName" />
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="排序值">
|
||||
<ElInputNumber v-model="model.sort" class="w-full" :min="0" :max="9999" placeholder="请输入排序值" />
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
|
||||
<div class="requirement-operate-dialog__col-right">
|
||||
<BusinessFormSection title="需求内容">
|
||||
<ElFormItem class="requirement-operate-dialog__desc-item">
|
||||
<BusinessRichTextEditor
|
||||
ref="richTextEditorRef"
|
||||
v-model="model.description"
|
||||
:height="editorHeight"
|
||||
upload-directory="requirement"
|
||||
placeholder="请输入需求内容"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="附件">
|
||||
<ElFormItem class="requirement-operate-dialog__attachment-item">
|
||||
<BusinessAttachmentUploader
|
||||
ref="attachmentUploaderRef"
|
||||
v-model="model.attachments"
|
||||
directory="requirement"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.requirement-operate-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.requirement-operate-dialog__col-left,
|
||||
.requirement-operate-dialog__col-right {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.requirement-operate-dialog__col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.requirement-operate-dialog__desc-item,
|
||||
.requirement-operate-dialog__attachment-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (width <= 1024px) {
|
||||
.requirement-operate-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user