Files
cn-rdms-web/src/views/personal-center/my-item/modules/personal-item-operate-dialog.vue
2026-05-21 10:44:00 +08:00

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>