feat(personal-item): 个人事项
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchCreatePersonalItem, fetchGetPersonalItemDetail, fetchUpdatePersonalItem } 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 { isEmptyRichText } from './personal-item-shared';
|
||||
|
||||
defineOptions({ name: 'PersonalItemOperateDialog' });
|
||||
|
||||
type PersonalItemOperateType = UI.TableOperateType | 'view';
|
||||
|
||||
interface Props {
|
||||
operateType: PersonalItemOperateType;
|
||||
rowData?: Api.PersonalItem.PersonalItem | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submitted: [];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const currentUserId = computed(() => authStore.userInfo.userId || 'current-user');
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
||||
const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null>(null);
|
||||
const leftColRef = ref<HTMLElement>();
|
||||
const editorHeight = ref<string>('45vh');
|
||||
|
||||
const ATTACHMENT_SECTION_RESERVE_PX = 140;
|
||||
|
||||
useResizeObserver(leftColRef, entries => {
|
||||
const h = entries[0]?.contentRect.height;
|
||||
if (h && h > 120) {
|
||||
editorHeight.value = `${Math.max(h - 60 - ATTACHMENT_SECTION_RESERVE_PX, 200)}px`;
|
||||
}
|
||||
});
|
||||
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
const isView = computed(() => props.operateType === 'view');
|
||||
const detailLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
interface Model {
|
||||
taskTitle: string;
|
||||
plannedStartDate: string | null;
|
||||
plannedEndDate: string | null;
|
||||
taskDesc: string | null;
|
||||
attachments: Api.Project.AttachmentItem[];
|
||||
}
|
||||
|
||||
const model = reactive<Model>(createDefaultModel());
|
||||
|
||||
const title = computed(() => {
|
||||
if (isView.value) {
|
||||
return '个人事项详情';
|
||||
}
|
||||
|
||||
return isEdit.value ? '编辑个人事项' : '新增个人事项';
|
||||
});
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
taskTitle: '',
|
||||
plannedStartDate: null,
|
||||
plannedEndDate: null,
|
||||
taskDesc: null,
|
||||
attachments: []
|
||||
};
|
||||
}
|
||||
|
||||
function isPlannedDateRangeValid(startDate: string | null, endDate: string | null) {
|
||||
if (!startDate || !endDate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !dayjs(endDate).isBefore(dayjs(startDate), 'day');
|
||||
}
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
taskTitle: [
|
||||
createRequiredRule('请输入事项标题'),
|
||||
{
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value?.trim()) {
|
||||
callback(new Error('请输入事项标题'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
plannedStartDate: [createRequiredRule('请选择计划开始日期')],
|
||||
plannedEndDate: [
|
||||
createRequiredRule('请选择计划结束日期'),
|
||||
{
|
||||
validator: (_rule, value: string | null, callback) => {
|
||||
if (!isPlannedDateRangeValid(model.plannedStartDate, value)) {
|
||||
callback(new Error('计划结束日期不能早于计划开始日期'));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}) satisfies Record<string, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
async function initModel() {
|
||||
detailLoading.value = true;
|
||||
|
||||
Object.assign(model, createDefaultModel());
|
||||
|
||||
if ((isEdit.value || isView.value) && props.rowData) {
|
||||
const { error, data } = await fetchGetPersonalItemDetail(props.rowData.id);
|
||||
|
||||
if (!error && data) {
|
||||
model.taskTitle = data.taskTitle;
|
||||
model.plannedStartDate = data.plannedStartDate;
|
||||
model.plannedEndDate = data.plannedEndDate;
|
||||
model.taskDesc = data.taskDesc;
|
||||
model.attachments = data.attachments ? [...data.attachments] : [];
|
||||
}
|
||||
}
|
||||
|
||||
detailLoading.value = false;
|
||||
|
||||
await nextTick();
|
||||
attachmentUploaderRef.value?.initSession();
|
||||
richTextEditorRef.value?.initSession();
|
||||
formRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (isView.value) {
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await validate();
|
||||
|
||||
if (attachmentUploaderRef.value?.hasUploading) {
|
||||
window.$message?.warning('附件正在上传中,请稍候');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Api.PersonalItem.SavePersonalItemParams = {
|
||||
taskTitle: model.taskTitle.trim(),
|
||||
ownerId: currentUserId.value,
|
||||
plannedStartDate: model.plannedStartDate,
|
||||
plannedEndDate: model.plannedEndDate,
|
||||
taskDesc: isEmptyRichText(model.taskDesc) ? null : (model.taskDesc ?? null),
|
||||
attachments: [...model.attachments]
|
||||
};
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
const result =
|
||||
isEdit.value && props.rowData
|
||||
? await fetchUpdatePersonalItem({ id: props.rowData.id, ...payload })
|
||||
: await fetchCreatePersonalItem(payload);
|
||||
|
||||
submitting.value = false;
|
||||
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]);
|
||||
|
||||
window.$message?.success(isEdit.value ? '个人事项修改成功' : '个人事项创建成功');
|
||||
visible.value = false;
|
||||
emit('submitted');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (value) {
|
||||
initModel();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="1100px"
|
||||
:loading="detailLoading"
|
||||
:confirm-loading="submitting"
|
||||
:show-footer="!isView"
|
||||
max-body-height="78vh"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
:validate-on-rule-change="false"
|
||||
class="personal-item-operate-dialog__form"
|
||||
>
|
||||
<div class="personal-item-operate-dialog__grid">
|
||||
<div ref="leftColRef" class="personal-item-operate-dialog__col-left">
|
||||
<BusinessFormSection title="事项信息">
|
||||
<ElFormItem label="事项标题" prop="taskTitle">
|
||||
<ElInput
|
||||
v-model="model.taskTitle"
|
||||
:clearable="!isView"
|
||||
:disabled="isView"
|
||||
maxlength="300"
|
||||
placeholder="请输入事项标题"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="计划开始日期" prop="plannedStartDate">
|
||||
<ElDatePicker
|
||||
v-model="model.plannedStartDate"
|
||||
:disabled="isView"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择计划开始日期"
|
||||
class="personal-item-operate-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="计划结束日期" prop="plannedEndDate">
|
||||
<ElDatePicker
|
||||
v-model="model.plannedEndDate"
|
||||
:disabled="isView"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择计划结束日期"
|
||||
class="personal-item-operate-dialog__date-picker"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
|
||||
<div class="personal-item-operate-dialog__col-right">
|
||||
<BusinessFormSection title="事项说明">
|
||||
<ElFormItem class="personal-item-operate-dialog__desc-item" prop="taskDesc">
|
||||
<BusinessRichTextEditor
|
||||
ref="richTextEditorRef"
|
||||
v-model="model.taskDesc"
|
||||
:height="editorHeight"
|
||||
:disabled="isView"
|
||||
upload-directory="personal-item"
|
||||
placeholder="请输入事项说明"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
|
||||
<BusinessFormSection title="附件">
|
||||
<ElFormItem class="personal-item-operate-dialog__attachment-item">
|
||||
<BusinessAttachmentUploader
|
||||
ref="attachmentUploaderRef"
|
||||
v-model="model.attachments"
|
||||
directory="personal-item"
|
||||
:disabled="isView"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</BusinessFormSection>
|
||||
</div>
|
||||
</div>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.personal-item-operate-dialog__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.personal-item-operate-dialog__col-left,
|
||||
.personal-item-operate-dialog__col-right {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.personal-item-operate-dialog__col-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.personal-item-operate-dialog__desc-item,
|
||||
.personal-item-operate-dialog__attachment-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (width <= 1024px) {
|
||||
.personal-item-operate-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.personal-item-operate-dialog__date-picker.el-date-editor.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user