Files
cn-rdms-web/src/views/product/requirement/modules/requirement-split-dialog.vue

365 lines
11 KiB
Vue

<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';
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';
defineOptions({ name: 'RequirementSplitDialog' });
interface Props {
parentRequirement: Api.Product.Requirement | null;
productId: string;
memberOptions: Api.Product.ProductMember[];
categoryDictCode: string;
priorityDictCode: string;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const { enabledDictData: priorityDictData } = useDict(() => props.priorityDictCode);
const 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 }
];
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;
attachments: Api.Project.AttachmentItem[];
reviewRequired: number;
category: string;
priority: number | null;
expectedTime: string | null;
currentHandlerUserId: string;
sort: number;
}
const submitting = ref(false);
const loading = ref(false);
const model = ref<Model>(createDefaultModel());
const memberUserOptions = computed(() => {
return props.memberOptions.filter(m => m.status === 0);
});
const rules = {
title: [createRequiredRule('请输入子需求名称')],
priority: [createRequiredRule('请选择优先级')],
currentHandlerUserId: [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(/&nbsp;/g, '')
.trim();
if (text) {
return false;
}
return !/<img\b/i.test(html);
}
function createDefaultModel(): Model {
return {
title: '',
description: null,
attachments: [],
reviewRequired: 0,
category: '',
priority: 1,
expectedTime: null,
currentHandlerUserId: '',
sort: 0
};
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
await validate();
if (!props.productId || !props.parentRequirement?.id) {
return;
}
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: props.parentRequirement.proposerNickname || '',
currentHandlerUserNickname: handler?.userNickname || '',
title: model.value.title.trim(),
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,
expectedTime: model.value.expectedTime,
currentHandlerUserId: model.value.currentHandlerUserId,
sort: model.value.sort
};
submitting.value = true;
const result = await fetchSplitRequirement(payload);
submitting.value = false;
if (result.error) {
return;
}
await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]);
window.$message?.success('需求拆分成功');
closeDialog();
emit('submitted');
}
watch(
() => visible.value,
async value => {
if (!value) {
return;
}
model.value = createDefaultModel();
if (props.parentRequirement?.category) {
model.value.category = props.parentRequirement.category;
}
if (props.parentRequirement?.currentHandlerUserId) {
model.value.currentHandlerUserId = props.parentRequirement.currentHandlerUserId;
}
if (props.parentRequirement?.expectedTime) {
model.value.expectedTime = props.parentRequirement.expectedTime;
}
await nextTick();
attachmentUploaderRef.value?.initSession();
richTextEditorRef.value?.initSession();
formRef.value?.clearValidate();
}
);
</script>
<template>
<BusinessFormDialog
v-model="visible"
title="拆分需求"
width="1100px"
max-body-height="78vh"
:loading="loading"
:confirm-loading="submitting"
@confirm="handleSubmit"
>
<ElAlert
v-if="parentRequirement"
:title="`正在拆分需求:${parentRequirement.title}`"
type="info"
:closable="false"
class="mb-16px"
/>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :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="是否需要评审">
<ElRadioGroup v-model="model.reviewRequired">
<ElRadio
v-for="item in reviewRequiredOptions"
:key="item.value"
:value="item.value"
border
style="width: 165px"
>
{{ item.label }}
</ElRadio>
</ElRadioGroup>
</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="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="预期完成时间">
<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>
<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>
.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;
}
}
:deep(.requirement-operate-dialog__date-picker.el-date-editor.el-input) {
width: 100%;
}
</style>