358 lines
8.7 KiB
Vue
358 lines
8.7 KiB
Vue
<script setup lang="ts">
|
|
import type { IDomEditor } from '@wangeditor/editor';
|
|
|
|
import { nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
|
|
|
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
|
import { $t } from '@easyflow/locales';
|
|
|
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
|
|
import DOMPurify from 'dompurify';
|
|
import {
|
|
ElForm,
|
|
ElFormItem,
|
|
ElInput,
|
|
ElMessage,
|
|
ElTreeSelect,
|
|
} from 'element-plus';
|
|
|
|
import { api } from '#/api/request';
|
|
import { buildKnowledgePath } from '#/views/ai/documentCollection/share-path';
|
|
|
|
import '@wangeditor/editor/dist/css/style.css';
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
data: {
|
|
type: Object as any,
|
|
default: () => ({}),
|
|
},
|
|
categoryOptions: {
|
|
type: Array as any,
|
|
default: () => [],
|
|
},
|
|
requestClient: {
|
|
type: Object as any,
|
|
default: () => api,
|
|
},
|
|
endpointPrefix: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(['submit', 'update:modelValue']);
|
|
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
const ALLOWED_IMAGE_TYPES = new Set([
|
|
'image/gif',
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/webp',
|
|
]);
|
|
|
|
const editorRef = shallowRef<IDomEditor | null>(null);
|
|
const form = ref<any>({
|
|
id: '',
|
|
collectionId: '',
|
|
categoryId: '',
|
|
question: '',
|
|
answerHtml: '',
|
|
orderNo: 0,
|
|
});
|
|
|
|
watch(
|
|
() => props.data,
|
|
(newData: any) => {
|
|
form.value = {
|
|
id: newData?.id || '',
|
|
collectionId: newData?.collectionId || '',
|
|
categoryId:
|
|
newData?.categoryId === undefined || newData?.categoryId === null
|
|
? ''
|
|
: String(newData.categoryId),
|
|
question: newData?.question || '',
|
|
answerHtml: newData?.answerHtml || '',
|
|
orderNo: newData?.orderNo ?? 0,
|
|
};
|
|
},
|
|
{ immediate: true, deep: true },
|
|
);
|
|
|
|
const toolbarConfig = {
|
|
excludeKeys: ['insertVideo', 'group-video', 'uploadVideo', 'todo', 'emotion'],
|
|
};
|
|
const editorConfig: any = {
|
|
placeholder: $t('documentCollection.faq.answerPlaceholder'),
|
|
MENU_CONF: {
|
|
uploadImage: {
|
|
customUpload: async (
|
|
file: File,
|
|
insertFn: (url: string, alt?: string, href?: string) => void,
|
|
) => {
|
|
if (!form.value.collectionId) {
|
|
ElMessage.error($t('documentCollection.faq.collectionRequired'));
|
|
return;
|
|
}
|
|
if (!ALLOWED_IMAGE_TYPES.has(file.type)) {
|
|
ElMessage.error($t('documentCollection.faq.imageTypeInvalid'));
|
|
return;
|
|
}
|
|
if (file.size > MAX_IMAGE_SIZE) {
|
|
ElMessage.error($t('documentCollection.faq.imageSizeExceeded'));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await props.requestClient.upload(
|
|
buildKnowledgePath(
|
|
props.endpointPrefix,
|
|
'/api/v1/faqItem/uploadImage',
|
|
),
|
|
{ collectionId: form.value.collectionId, file },
|
|
{},
|
|
);
|
|
const imageUrl = res?.data?.path;
|
|
if (!imageUrl) {
|
|
ElMessage.error($t('documentCollection.faq.imageUploadFailed'));
|
|
return;
|
|
}
|
|
insertFn(imageUrl, file.name, imageUrl);
|
|
} catch (error) {
|
|
console.error('FAQ image upload failed:', error);
|
|
ElMessage.error($t('documentCollection.faq.imageUploadFailed'));
|
|
}
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const handleEditorCreated = (editor: IDomEditor) => {
|
|
editorRef.value = editor;
|
|
};
|
|
|
|
const focusAnswerEditor = () => {
|
|
const editor = editorRef.value as any;
|
|
if (editor && typeof editor.focus === 'function') {
|
|
try {
|
|
editor.focus(true);
|
|
} catch {
|
|
editor.focus();
|
|
}
|
|
}
|
|
nextTick(() => {
|
|
const editableEl = document.querySelector(
|
|
'.faq-edit-dialog .w-e-text-container [contenteditable="true"]',
|
|
) as HTMLElement | null;
|
|
editableEl?.focus();
|
|
});
|
|
};
|
|
|
|
const handleQuestionKeydown = (event: Event | KeyboardEvent) => {
|
|
if (
|
|
event instanceof KeyboardEvent &&
|
|
event.key === 'Tab' &&
|
|
!event.shiftKey
|
|
) {
|
|
event.preventDefault();
|
|
focusAnswerEditor();
|
|
}
|
|
};
|
|
|
|
const closeDialog = () => {
|
|
emit('update:modelValue', false);
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (!form.value.question?.trim()) {
|
|
ElMessage.error($t('documentCollection.faq.questionRequired'));
|
|
return;
|
|
}
|
|
const sanitizedHtml = DOMPurify.sanitize(form.value.answerHtml || '', {
|
|
ADD_TAGS: ['img'],
|
|
ADD_ATTR: ['src', 'alt', 'title', 'width', 'height'],
|
|
ALLOWED_URI_REGEXP: /^https?:\/\//i,
|
|
});
|
|
const doc = new DOMParser().parseFromString(sanitizedHtml, 'text/html');
|
|
const pureText = (doc.body?.textContent || '').trim();
|
|
const hasImage = doc.querySelector('img[src]') !== null;
|
|
if (!pureText && !hasImage) {
|
|
ElMessage.error($t('documentCollection.faq.answerRequired'));
|
|
return;
|
|
}
|
|
emit('submit', {
|
|
...form.value,
|
|
question: form.value.question.trim(),
|
|
answerHtml: sanitizedHtml,
|
|
orderNo: Number(form.value.orderNo) || 0,
|
|
categoryId: form.value.categoryId || null,
|
|
});
|
|
};
|
|
|
|
onBeforeUnmount(() => {
|
|
const editor = editorRef.value;
|
|
if (editor) {
|
|
editor.destroy();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<EasyFlowFormModal
|
|
class="faq-edit-dialog"
|
|
:open="modelValue"
|
|
:title="form.id ? $t('button.edit') : $t('button.add')"
|
|
width="min(920px, 92vw)"
|
|
:confirm-text="$t('button.save')"
|
|
@update:open="emit('update:modelValue', $event)"
|
|
@cancel="closeDialog"
|
|
@confirm="handleSubmit"
|
|
>
|
|
<ElForm
|
|
class="faq-form easyflow-modal-form easyflow-modal-form--compact"
|
|
label-position="top"
|
|
>
|
|
<ElFormItem :label="$t('documentCollection.faq.category')">
|
|
<ElTreeSelect
|
|
v-model="form.categoryId"
|
|
check-strictly
|
|
clearable
|
|
:data="categoryOptions"
|
|
node-key="id"
|
|
:props="{ label: 'categoryName', children: 'children' }"
|
|
:placeholder="$t('documentCollection.faq.categoryPlaceholder')"
|
|
/>
|
|
</ElFormItem>
|
|
<ElFormItem :label="$t('documentCollection.faq.question')">
|
|
<ElInput
|
|
v-model="form.question"
|
|
:placeholder="$t('documentCollection.faq.questionPlaceholder')"
|
|
@keydown="handleQuestionKeydown"
|
|
/>
|
|
<div class="field-tip">Tab 可快速跳转到答案编辑区域</div>
|
|
</ElFormItem>
|
|
<ElFormItem :label="$t('documentCollection.faq.answer')">
|
|
<div class="editor-wrapper">
|
|
<Toolbar
|
|
:editor="editorRef"
|
|
:default-config="toolbarConfig"
|
|
mode="default"
|
|
/>
|
|
<Editor
|
|
v-model="form.answerHtml"
|
|
:default-config="editorConfig"
|
|
mode="default"
|
|
@on-created="handleEditorCreated"
|
|
/>
|
|
</div>
|
|
</ElFormItem>
|
|
</ElForm>
|
|
</EasyFlowFormModal>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.faq-form :deep(.el-form-item__label) {
|
|
padding-bottom: 8px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.faq-form :deep(.el-form-item) {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.faq-form :deep(.el-input__wrapper),
|
|
.faq-form :deep(.el-select__wrapper) {
|
|
min-height: 42px;
|
|
border-radius: 10px;
|
|
transition: box-shadow 0.2s ease;
|
|
}
|
|
|
|
.faq-form :deep(.el-input__wrapper.is-focus),
|
|
.faq-form :deep(.el-select__wrapper.is-focused) {
|
|
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
|
}
|
|
|
|
.field-tip {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
.editor-wrapper {
|
|
width: 100%;
|
|
overflow: hidden;
|
|
background: var(--el-fill-color-blank);
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
border-radius: 12px;
|
|
box-shadow: 0 8px 24px rgb(15 23 42 / 4%);
|
|
transition:
|
|
border-color 0.2s ease,
|
|
box-shadow 0.2s ease;
|
|
}
|
|
|
|
.editor-wrapper:focus-within {
|
|
border-color: var(--el-color-primary);
|
|
box-shadow: 0 0 0 3px rgb(64 158 255 / 12%);
|
|
}
|
|
|
|
:deep(.w-e-toolbar) {
|
|
padding: 8px 10px;
|
|
background: var(--el-fill-color-blank);
|
|
border-bottom: 1px solid var(--el-border-color-light);
|
|
}
|
|
|
|
:deep(.w-e-text-container) {
|
|
height: 340px;
|
|
min-height: 340px;
|
|
cursor: text;
|
|
background: var(--el-fill-color-blank);
|
|
}
|
|
|
|
:deep(.w-e-text-container .w-e-scroll) {
|
|
cursor: text;
|
|
}
|
|
|
|
:deep(.w-e-text-container [data-slate-editor]) {
|
|
min-height: 100%;
|
|
padding: 12px;
|
|
cursor: text;
|
|
}
|
|
|
|
:deep(.w-e-text-container [data-slate-editor] p) {
|
|
cursor: text;
|
|
}
|
|
|
|
.dialog-footer {
|
|
display: flex;
|
|
gap: 12px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.footer-btn {
|
|
min-width: 88px;
|
|
}
|
|
|
|
:deep(.faq-edit-dialog .el-dialog) {
|
|
overflow: hidden;
|
|
border-radius: 14px;
|
|
}
|
|
|
|
:deep(.faq-edit-dialog .el-dialog__header) {
|
|
padding: 18px 22px;
|
|
margin-right: 0;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
|
|
:deep(.faq-edit-dialog .el-dialog__body) {
|
|
padding: 18px 22px 16px;
|
|
}
|
|
|
|
:deep(.faq-edit-dialog .el-dialog__footer) {
|
|
padding: 12px 22px 18px;
|
|
border-top: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
</style>
|