326 lines
9.1 KiB
Vue
326 lines
9.1 KiB
Vue
<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>
|