feat(projects): 增加意见反馈
This commit is contained in:
252
src/views/feedback/modules/feedback-operate-dialog.vue
Normal file
252
src/views/feedback/modules/feedback-operate-dialog.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user