253 lines
7.3 KiB
Vue
253 lines
7.3 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
|||
|
|
import { FEEDBACK_TYPE_DICT_CODE } from '@/constants/dict';
|
|||
|
|
import { fetchCreateFeedback, fetchUpdateFeedback } from '@/service/api';
|
|||
|
|
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 DictSelect from '@/components/custom/dict-select.vue';
|
|||
|
|
|
|||
|
|
defineOptions({ name: 'FeedbackOperateDialog' });
|
|||
|
|
|
|||
|
|
type OperateMode = 'create' | 'edit';
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
mode: OperateMode;
|
|||
|
|
rowData?: Api.Feedback.FeedbackItem | null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
rowData: null
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
submitted: [];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const visible = defineModel<boolean>('visible', { default: false });
|
|||
|
|
|
|||
|
|
const { formRef, validate } = useForm();
|
|||
|
|
const { createRequiredRule } = useFormRules();
|
|||
|
|
|
|||
|
|
const submitting = ref(false);
|
|||
|
|
const attachmentUploaderRef = ref<InstanceType<typeof BusinessAttachmentUploader> | null>(null);
|
|||
|
|
const richTextEditorRef = ref<InstanceType<typeof BusinessRichTextEditor> | null>(null);
|
|||
|
|
|
|||
|
|
interface FormModel {
|
|||
|
|
/** DictSelect 选中后为字符串;提交时 Number() 转字典 Integer(非 ID,允许转 number) */
|
|||
|
|
type: string | number | null;
|
|||
|
|
title: string;
|
|||
|
|
content: string;
|
|||
|
|
contact: string;
|
|||
|
|
attachments: Api.Project.AttachmentItem[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const model = reactive<FormModel>({
|
|||
|
|
type: null,
|
|||
|
|
title: '',
|
|||
|
|
content: '',
|
|||
|
|
contact: '',
|
|||
|
|
attachments: []
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const dialogTitle = computed(() => (props.mode === 'create' ? '提交意见反馈' : '编辑意见反馈'));
|
|||
|
|
|
|||
|
|
/** 富文本是否为空:去标签去 后无文本,且无图片 */
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const rules = computed(
|
|||
|
|
() =>
|
|||
|
|
({
|
|||
|
|
type: [createRequiredRule('请选择反馈分类')],
|
|||
|
|
title: [createRequiredRule('请输入标题')],
|
|||
|
|
content: [
|
|||
|
|
{
|
|||
|
|
validator: (_rule, _value, callback) => {
|
|||
|
|
if (isEmptyRichText(model.content)) {
|
|||
|
|
callback(new Error('请输入详细描述'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
callback();
|
|||
|
|
},
|
|||
|
|
trigger: 'blur'
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}) satisfies Record<string, App.Global.FormRule[]>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
function applyRowDataToModel() {
|
|||
|
|
const row = props.rowData;
|
|||
|
|
if (props.mode === 'edit' && row) {
|
|||
|
|
// 字典选项 value 为字符串,回显需转字符串才能与下拉项严格匹配(否则显示原始数字)
|
|||
|
|
model.type = row.type === null || row.type === undefined ? null : String(row.type);
|
|||
|
|
model.title = row.title ?? '';
|
|||
|
|
model.content = row.content ?? '';
|
|||
|
|
model.contact = row.contact ?? '';
|
|||
|
|
model.attachments = row.attachments ? [...row.attachments] : [];
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
model.type = null;
|
|||
|
|
model.title = '';
|
|||
|
|
model.content = '';
|
|||
|
|
model.contact = '';
|
|||
|
|
model.attachments = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function handleConfirm() {
|
|||
|
|
await validate();
|
|||
|
|
|
|||
|
|
if (attachmentUploaderRef.value?.hasUploading) {
|
|||
|
|
window.$message?.warning('附件还在上传中,请稍候');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
submitting.value = true;
|
|||
|
|
|
|||
|
|
const basePayload: Api.Feedback.FeedbackSubmitParams = {
|
|||
|
|
type: Number(model.type),
|
|||
|
|
title: model.title.trim(),
|
|||
|
|
content: model.content,
|
|||
|
|
contact: model.contact.trim() || undefined,
|
|||
|
|
attachments: [...model.attachments]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { error } =
|
|||
|
|
props.mode === 'edit' && props.rowData
|
|||
|
|
? await fetchUpdateFeedback({ id: props.rowData.id, ...basePayload })
|
|||
|
|
: await fetchCreateFeedback(basePayload);
|
|||
|
|
|
|||
|
|
submitting.value = false;
|
|||
|
|
|
|||
|
|
if (error) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 成功后先 commit,置 committed=true,避免 destroy-on-close 触发 rollback 误删已存文件/图片
|
|||
|
|
await Promise.all([attachmentUploaderRef.value?.commit(), richTextEditorRef.value?.commit()]);
|
|||
|
|
window.$message?.success(props.mode === 'edit' ? '保存成功' : '提交成功');
|
|||
|
|
visible.value = false;
|
|||
|
|
emit('submitted');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function handleCancel() {
|
|||
|
|
await Promise.all([attachmentUploaderRef.value?.rollback(), richTextEditorRef.value?.rollback()]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(visible, async value => {
|
|||
|
|
if (!value) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
applyRowDataToModel();
|
|||
|
|
await nextTick();
|
|||
|
|
// 让附件 / 富文本组件把当前 model 视作 original,必须在 model 填充之后
|
|||
|
|
attachmentUploaderRef.value?.initSession();
|
|||
|
|
richTextEditorRef.value?.initSession();
|
|||
|
|
formRef.value?.clearValidate();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<BusinessFormDialog
|
|||
|
|
v-model="visible"
|
|||
|
|
:title="dialogTitle"
|
|||
|
|
width="980px"
|
|||
|
|
max-body-height="76vh"
|
|||
|
|
:confirm-loading="submitting"
|
|||
|
|
@confirm="handleConfirm"
|
|||
|
|
@cancel="handleCancel"
|
|||
|
|
>
|
|||
|
|
<ElForm
|
|||
|
|
ref="formRef"
|
|||
|
|
:model="model"
|
|||
|
|
:rules="rules"
|
|||
|
|
label-position="top"
|
|||
|
|
:validate-on-rule-change="false"
|
|||
|
|
class="feedback-operate-dialog__form"
|
|||
|
|
>
|
|||
|
|
<div class="feedback-operate-dialog__grid">
|
|||
|
|
<div class="feedback-operate-dialog__col-left">
|
|||
|
|
<BusinessFormSection title="反馈信息">
|
|||
|
|
<ElFormItem label="反馈分类" prop="type">
|
|||
|
|
<DictSelect v-model="model.type" :dict-code="FEEDBACK_TYPE_DICT_CODE" placeholder="请选择反馈分类" />
|
|||
|
|
</ElFormItem>
|
|||
|
|
|
|||
|
|
<ElFormItem label="标题" prop="title">
|
|||
|
|
<ElInput v-model="model.title" maxlength="100" show-word-limit placeholder="请输入标题" />
|
|||
|
|
</ElFormItem>
|
|||
|
|
|
|||
|
|
<ElFormItem label="联系方式">
|
|||
|
|
<ElInput v-model="model.contact" maxlength="100" placeholder="选填,方便我们联系你" />
|
|||
|
|
</ElFormItem>
|
|||
|
|
</BusinessFormSection>
|
|||
|
|
|
|||
|
|
<BusinessFormSection title="附件">
|
|||
|
|
<ElFormItem class="feedback-operate-dialog__attachment-item">
|
|||
|
|
<BusinessAttachmentUploader
|
|||
|
|
ref="attachmentUploaderRef"
|
|||
|
|
v-model="model.attachments"
|
|||
|
|
directory="feedback"
|
|||
|
|
/>
|
|||
|
|
</ElFormItem>
|
|||
|
|
</BusinessFormSection>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="feedback-operate-dialog__col-right">
|
|||
|
|
<BusinessFormSection title="详细描述">
|
|||
|
|
<ElFormItem prop="content" class="feedback-operate-dialog__desc-item">
|
|||
|
|
<BusinessRichTextEditor
|
|||
|
|
ref="richTextEditorRef"
|
|||
|
|
v-model="model.content"
|
|||
|
|
:height="380"
|
|||
|
|
upload-directory="feedback"
|
|||
|
|
placeholder="请输入详细描述"
|
|||
|
|
/>
|
|||
|
|
</ElFormItem>
|
|||
|
|
</BusinessFormSection>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</ElForm>
|
|||
|
|
</BusinessFormDialog>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.feedback-operate-dialog__grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 320px 1fr;
|
|||
|
|
gap: 24px;
|
|||
|
|
align-items: start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.feedback-operate-dialog__col-left,
|
|||
|
|
.feedback-operate-dialog__col-right {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16px;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.feedback-operate-dialog__desc-item,
|
|||
|
|
.feedback-operate-dialog__attachment-item {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (width <= 1024px) {
|
|||
|
|
.feedback-operate-dialog__grid {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|