|
@@ -1,13 +1,33 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="editor">
|
|
<div class="editor">
|
|
|
- <Toolbar class="tool_bar" ref="toolbarRef"
|
|
|
|
|
- style="position: sticky; top: 0; z-index: 1; border-bottom: 1px solid #ccc" :editor="editorRef"
|
|
|
|
|
- :defaultConfig="toolbarConfig" :mode="mode" />
|
|
|
|
|
- <Editor class="editor_content" ref="editorContentRef" v-model="valueHtml" :defaultConfig="editorConfig" :mode="mode"
|
|
|
|
|
- @onCreated="handleCreated" @onChange="handleChange" />
|
|
|
|
|
|
|
+ <Toolbar
|
|
|
|
|
+ class="tool_bar"
|
|
|
|
|
+ ref="toolbarRef"
|
|
|
|
|
+ style="position: sticky; top: 0; z-index: 1; border-bottom: 1px solid #ccc"
|
|
|
|
|
+ :editor="editorRef"
|
|
|
|
|
+ :defaultConfig="toolbarConfig"
|
|
|
|
|
+ :mode="mode"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Editor
|
|
|
|
|
+ class="editor_content"
|
|
|
|
|
+ ref="editorContentRef"
|
|
|
|
|
+ v-model="valueHtml"
|
|
|
|
|
+ :defaultConfig="editorConfig"
|
|
|
|
|
+ :mode="mode"
|
|
|
|
|
+ @onCreated="handleCreated"
|
|
|
|
|
+ @onChange="handleChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ <!-- 隐藏的图片上传input -->
|
|
|
|
|
+ <input type="file" ref="imageUploadRef" accept="image/*" style="display: none" @change="handleImageUpload" />
|
|
|
</div>
|
|
</div>
|
|
|
- <AIModal v-if="show_ai_modal" :show_modal="show_ai_modal" :target_active_pElement="target_active_pElement"
|
|
|
|
|
- @close="closeAIModal" @changePElement="changePElement" @insertPElement="insertPElement"></AIModal>
|
|
|
|
|
|
|
+ <AIModal
|
|
|
|
|
+ v-if="show_ai_modal"
|
|
|
|
|
+ :show_modal="show_ai_modal"
|
|
|
|
|
+ :target_active_pElement="target_active_pElement"
|
|
|
|
|
+ @close="closeAIModal"
|
|
|
|
|
+ @changePElement="changePElement"
|
|
|
|
|
+ @insertPElement="insertPElement"
|
|
|
|
|
+ ></AIModal>
|
|
|
</template>
|
|
</template>
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
|
|
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
|
|
@@ -27,31 +47,69 @@ const emit = defineEmits(['showMenus']);
|
|
|
|
|
|
|
|
// 编辑器实例,必须用 shallowRef
|
|
// 编辑器实例,必须用 shallowRef
|
|
|
const editorRef = shallowRef();
|
|
const editorRef = shallowRef();
|
|
|
-
|
|
|
|
|
-const toolbarRef = ref(null)
|
|
|
|
|
-
|
|
|
|
|
-// 正文内容
|
|
|
|
|
|
|
+const toolbarRef = ref(null);
|
|
|
const editorContentRef = ref();
|
|
const editorContentRef = ref();
|
|
|
|
|
+// 图片上传input的ref
|
|
|
|
|
+const imageUploadRef = ref(null);
|
|
|
|
|
|
|
|
// 内容 HTML
|
|
// 内容 HTML
|
|
|
-const valueHtml = ref('<p>hello</p>');
|
|
|
|
|
|
|
+const valueHtml = ref('');
|
|
|
// 目录
|
|
// 目录
|
|
|
const menus = ref([]);
|
|
const menus = ref([]);
|
|
|
|
|
|
|
|
// 当前要AI转写的P标签
|
|
// 当前要AI转写的P标签
|
|
|
const target_active_pElement = ref(null);
|
|
const target_active_pElement = ref(null);
|
|
|
-
|
|
|
|
|
const show_ai_modal = ref(false);
|
|
const show_ai_modal = ref(false);
|
|
|
|
|
|
|
|
// 模拟 ajax 异步获取内容
|
|
// 模拟 ajax 异步获取内容
|
|
|
-onMounted(() => {
|
|
|
|
|
-});
|
|
|
|
|
|
|
+onMounted(() => {});
|
|
|
|
|
|
|
|
-const toolbarConfig = {}
|
|
|
|
|
|
|
+const toolbarConfig = {
|
|
|
|
|
+ // 保留图片上传按钮
|
|
|
|
|
+};
|
|
|
const editorConfig = {
|
|
const editorConfig = {
|
|
|
placeholder: '请输入内容...',
|
|
placeholder: '请输入内容...',
|
|
|
-}
|
|
|
|
|
|
|
+ MENU_CONF: {
|
|
|
|
|
+ editImage: {
|
|
|
|
|
+ hideLinkImg: true // 隐藏图片链接按钮
|
|
|
|
|
+ },
|
|
|
|
|
+ // 配置图片上传
|
|
|
|
|
+ uploadImage: {
|
|
|
|
|
+ // 自定义上传逻辑
|
|
|
|
|
+ customUpload: async (file, insertFn) => {
|
|
|
|
|
+ // 验证文件类型
|
|
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
|
|
+ alert('请选择图片文件');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证文件大小 (限制10MB)
|
|
|
|
|
+ const maxSize = 10 * 1024 * 1024; // 10MB
|
|
|
|
|
+ if (file.size > maxSize) {
|
|
|
|
|
+ alert('图片大小不能超过10MB');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 这里我们将使用base64处理,不发送真实请求
|
|
|
|
|
+ const base64Url = await convertImageToBase64(file);
|
|
|
|
|
|
|
|
|
|
+ // 检查base64数据是否有效
|
|
|
|
|
+ if (!base64Url.startsWith('data:image/')) {
|
|
|
|
|
+ alert('图片转换失败,请重试');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 插入图片到编辑器
|
|
|
|
|
+ insertFn(base64Url, file.name, file.size);
|
|
|
|
|
+
|
|
|
|
|
+ // 图片插入后重新绑定事件
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ bindPElementEvents();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
// 组件销毁时,也及时销毁编辑器
|
|
// 组件销毁时,也及时销毁编辑器
|
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
@@ -62,62 +120,134 @@ onBeforeUnmount(() => {
|
|
|
|
|
|
|
|
const handleCreated = (editor) => {
|
|
const handleCreated = (editor) => {
|
|
|
editorRef.value = editor; // 记录 editor 实例,重要!
|
|
editorRef.value = editor; // 记录 editor 实例,重要!
|
|
|
|
|
+
|
|
|
|
|
+ // 修改工具栏配置,保留图片上传按钮
|
|
|
toolbarConfig.excludeKeys = [
|
|
toolbarConfig.excludeKeys = [
|
|
|
- "insertImage",
|
|
|
|
|
|
|
+ 'insertImage',
|
|
|
'fullScreen',
|
|
'fullScreen',
|
|
|
'enter',
|
|
'enter',
|
|
|
'emotion',
|
|
'emotion',
|
|
|
'group-video',
|
|
'group-video',
|
|
|
- 'todo'
|
|
|
|
|
- ]
|
|
|
|
|
- console.log('工具栏默认配置:', editor.getAllMenuKeys());
|
|
|
|
|
- console.log('工具栏默认配置:', editor.getConfig());
|
|
|
|
|
- console.log('工具栏默认配置:', Toolbar);
|
|
|
|
|
|
|
+ 'todo',
|
|
|
|
|
+ 'unLink',
|
|
|
|
|
+ 'viewLink',
|
|
|
|
|
+ 'insertLink',
|
|
|
|
|
+ 'insertTable',
|
|
|
|
|
+ 'editImage',
|
|
|
|
|
+ 'editLink',
|
|
|
|
|
+ 'viewImageLink'
|
|
|
|
|
+ ];
|
|
|
|
|
+ console.log(editor.getAllMenuKeys());
|
|
|
|
|
|
|
|
- // console.log(toolbarRef,.getConfig().toolbarKeys)
|
|
|
|
|
valueHtml.value = str_html;
|
|
valueHtml.value = str_html;
|
|
|
|
|
+ // 让所有的h1居中
|
|
|
|
|
+ toAlginCenterByH1();
|
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
|
- const pTags = getAllPTags(editorContentRef.value.$el);
|
|
|
|
|
- pTags?.forEach((pTag) => {
|
|
|
|
|
- pTag.addEventListener('mouseenter', () => handleMouseEnter(pTag));
|
|
|
|
|
- pTag.addEventListener('mouseleave', (e) => handleParentMouseLeave(e, pTag));
|
|
|
|
|
- });
|
|
|
|
|
- // const ele = document.querySelector('.editor_content');
|
|
|
|
|
- // if (ele) {
|
|
|
|
|
- // ele.addEventListener('mouseenter', (e) => {
|
|
|
|
|
- // const e_target = e.target;
|
|
|
|
|
- // console.log(e_ta);
|
|
|
|
|
- // });
|
|
|
|
|
- // }
|
|
|
|
|
|
|
+ bindPElementEvents();
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
+
|
|
|
const handleChange = () => {
|
|
const handleChange = () => {
|
|
|
menus.value = generateTableOfContents();
|
|
menus.value = generateTableOfContents();
|
|
|
emit('showMenus', menus);
|
|
emit('showMenus', menus);
|
|
|
|
|
+
|
|
|
|
|
+ // 内容变化时重新绑定事件
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ bindPElementEvents();
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 统一绑定P元素事件的函数
|
|
|
|
|
+const bindPElementEvents = () => {
|
|
|
|
|
+ if (!editorContentRef.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 先移除所有已存在的事件监听和AI按钮
|
|
|
|
|
+ const oldPTags = getAllPTags(editorContentRef.value.$el);
|
|
|
|
|
+ oldPTags?.forEach((pTag) => {
|
|
|
|
|
+ // 移除事件监听
|
|
|
|
|
+ pTag.removeEventListener('mouseenter', handleMouseEnter);
|
|
|
|
|
+ pTag.removeEventListener('mouseleave', handleParentMouseLeave);
|
|
|
|
|
+
|
|
|
|
|
+ // 移除AI按钮
|
|
|
|
|
+ const existingBtn = pTag.querySelector('.ai-btn');
|
|
|
|
|
+ if (existingBtn) {
|
|
|
|
|
+ pTag.removeChild(existingBtn);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置背景色
|
|
|
|
|
+ pTag.style.background = '#fff';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 重新绑定事件
|
|
|
|
|
+ const pTags = getAllPTags(editorContentRef.value.$el);
|
|
|
|
|
+ pTags?.forEach((pTag) => {
|
|
|
|
|
+ // 使用箭头函数包装,确保正确的this绑定
|
|
|
|
|
+ pTag.addEventListener('mouseenter', () => handleMouseEnter(pTag));
|
|
|
|
|
+ pTag.addEventListener('mouseleave', (e) => handleParentMouseLeave(e, pTag));
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+// 获取到文档中前几个H1进行居中
|
|
|
|
|
+const toAlginCenterByH1 = () => {
|
|
|
|
|
+ const max_count = 10;
|
|
|
|
|
+ let count = 0;
|
|
|
|
|
+ let interval = null;
|
|
|
|
|
+ interval = setInterval(() => {
|
|
|
|
|
+ if (count > max_count) {
|
|
|
|
|
+ clearInterval(interval);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ count += 1;
|
|
|
|
|
+ if (editorRef.value.getHtml()) {
|
|
|
|
|
+ console.log(document.querySelectorAll('.editor_content h1'));
|
|
|
|
|
+ const headers = document.querySelectorAll('.editor_content h1');
|
|
|
|
|
+ headers.forEach((item, index) => {
|
|
|
|
|
+ if (index + 1 == item.id.split('-')[1]) {
|
|
|
|
|
+ item.setAttribute('style', 'text-align: center ');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (item.id.split('-')[1] / 1 === 1) {
|
|
|
|
|
+ console.log(item.innerText);
|
|
|
|
|
+ emit('renderTitle', item.innerText);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ editorRef.value.updateView();
|
|
|
|
|
+ count = 11;
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 300);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const generateTableOfContents = () => {
|
|
const generateTableOfContents = () => {
|
|
|
- const headings = document.querySelectorAll('.editor_content h1, .editor_content h2, .editor_content h3');
|
|
|
|
|
- console.log(headings);
|
|
|
|
|
|
|
+ const headings = document.querySelectorAll(
|
|
|
|
|
+ '.editor_content h1, .editor_content h2, .editor_content h3,.editor_content h4,.editor_content h5'
|
|
|
|
|
+ );
|
|
|
const toc = [];
|
|
const toc = [];
|
|
|
headings.forEach((heading, index) => {
|
|
headings.forEach((heading, index) => {
|
|
|
const id = `section-${index + 1}`;
|
|
const id = `section-${index + 1}`;
|
|
|
- const level = heading.tagName === 'H1' ? 1 : heading.tagName === 'H2' ? 2 : 3; // 根据标题等级设置目录项的缩进
|
|
|
|
|
- heading.setAttribute('id', id); // 设置标题的id属性
|
|
|
|
|
- toc.push({ id: id, text: heading.textContent, level: level, index: index }); // 将标题文本、id和等级添加到目录项中
|
|
|
|
|
|
|
+ const level =
|
|
|
|
|
+ heading.tagName === 'H1'
|
|
|
|
|
+ ? 1
|
|
|
|
|
+ : heading.tagName === 'H2'
|
|
|
|
|
+ ? 2
|
|
|
|
|
+ : heading.tagName === 'H3'
|
|
|
|
|
+ ? 3
|
|
|
|
|
+ : heading.tagName === 'H4'
|
|
|
|
|
+ ? 4
|
|
|
|
|
+ : 5;
|
|
|
|
|
+ heading.setAttribute('id', id);
|
|
|
|
|
+ toc.push({ id: id, text: heading.textContent, level: level, index: index });
|
|
|
});
|
|
});
|
|
|
return toc;
|
|
return toc;
|
|
|
};
|
|
};
|
|
|
|
|
+
|
|
|
const getHTML = () => {
|
|
const getHTML = () => {
|
|
|
console.log(editorRef.value.getHtml());
|
|
console.log(editorRef.value.getHtml());
|
|
|
menus.value = generateTableOfContents();
|
|
menus.value = generateTableOfContents();
|
|
|
emit('showMenus', menus);
|
|
emit('showMenus', menus);
|
|
|
};
|
|
};
|
|
|
|
|
+
|
|
|
const handleItemClick = (item) => {
|
|
const handleItemClick = (item) => {
|
|
|
const editorContent = document.querySelector('.editor_content');
|
|
const editorContent = document.querySelector('.editor_content');
|
|
|
const targetElement = document.getElementById(item.id);
|
|
const targetElement = document.getElementById(item.id);
|
|
|
if (targetElement && editorContent) {
|
|
if (targetElement && editorContent) {
|
|
|
- console.dir(editorContent, 1111);
|
|
|
|
|
targetElement.scrollIntoView({
|
|
targetElement.scrollIntoView({
|
|
|
behavior: 'smooth',
|
|
behavior: 'smooth',
|
|
|
block: 'start'
|
|
block: 'start'
|
|
@@ -141,42 +271,53 @@ const getAllPTags = (parentNode) => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleMouseEnter = (pElement) => {
|
|
const handleMouseEnter = (pElement) => {
|
|
|
- if (!pElement.innerText.trim()) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- pElement.style.background = 'rgba(10, 129, 126, 0.3)';
|
|
|
|
|
- // 若已存在按钮,则不再添加
|
|
|
|
|
- const existingBtn = pElement.querySelector('.ai-btn');
|
|
|
|
|
- if (!existingBtn) {
|
|
|
|
|
- const btn = document.createElement('img');
|
|
|
|
|
- // btn.innerText = 'AI 转写';
|
|
|
|
|
- btn.classList.add('ai-btn');
|
|
|
|
|
- btn.style.position = 'absolute';
|
|
|
|
|
- btn.style.height = '40px';
|
|
|
|
|
- // btn.style.height = '30px';
|
|
|
|
|
- btn.style.top = '-10px';
|
|
|
|
|
- btn.style.left = '-45px';
|
|
|
|
|
- btn.style.padding = '5px 10px';
|
|
|
|
|
- // btn.style.fontSize = '14px';
|
|
|
|
|
- btn.src = RobotIcon;
|
|
|
|
|
- btn.style.color = '#fff';
|
|
|
|
|
- btn.style.border = 'none';
|
|
|
|
|
- btn.style.cursor = 'pointer';
|
|
|
|
|
- btn.style.borderRadius = '4px';
|
|
|
|
|
- btn.addEventListener('click', () => {
|
|
|
|
|
- target_active_pElement.value = pElement;
|
|
|
|
|
- // alert('AI 转写');
|
|
|
|
|
- show_ai_modal.value = true;
|
|
|
|
|
- });
|
|
|
|
|
- //设置p元素为相对定位,确保按钮能相对于P元素定位
|
|
|
|
|
- pElement.style.position = 'relative';
|
|
|
|
|
- pElement.appendChild(btn);
|
|
|
|
|
- btn.addEventListener('mouseleave', () => handleMouseLeave(pElement));
|
|
|
|
|
|
|
+ const hasImg = pElement.querySelector('img') !== null;
|
|
|
|
|
+ if (!hasImg) {
|
|
|
|
|
+ // 添加光标到段落末尾
|
|
|
|
|
+ const range = document.createRange();
|
|
|
|
|
+ const selection = window.getSelection();
|
|
|
|
|
+ range.selectNodeContents(pElement);
|
|
|
|
|
+ range.collapse(false); // 折叠到元素末尾
|
|
|
|
|
+ selection.removeAllRanges();
|
|
|
|
|
+ selection.addRange(range);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (!pElement.innerText.trim()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ pElement.style.background = 'rgba(10, 129, 126, 0.3)';
|
|
|
|
|
+ // 若已存在按钮,则不再添加
|
|
|
|
|
+ const existingBtn = pElement.querySelector('.ai-btn');
|
|
|
|
|
+ if (!existingBtn) {
|
|
|
|
|
+ const btn = document.createElement('img');
|
|
|
|
|
+ btn.classList.add('ai-btn');
|
|
|
|
|
+ btn.style.position = 'absolute';
|
|
|
|
|
+ btn.style.height = '40px';
|
|
|
|
|
+ btn.style.top = '-10px';
|
|
|
|
|
+ btn.style.left = '-45px';
|
|
|
|
|
+ btn.style.padding = '5px 10px';
|
|
|
|
|
+ btn.src = RobotIcon;
|
|
|
|
|
+ btn.style.color = '#fff';
|
|
|
|
|
+ btn.style.border = 'none';
|
|
|
|
|
+ btn.style.cursor = 'pointer';
|
|
|
|
|
+ btn.style.borderRadius = '4px';
|
|
|
|
|
+ btn.addEventListener('click', () => {
|
|
|
|
|
+ target_active_pElement.value = pElement;
|
|
|
|
|
+ show_ai_modal.value = true;
|
|
|
|
|
+ });
|
|
|
|
|
+ //设置p元素为相对定位,确保按钮能相对于P元素定位
|
|
|
|
|
+ pElement.style.position = 'relative';
|
|
|
|
|
+ pElement.appendChild(btn);
|
|
|
|
|
+ btn.addEventListener('mouseleave', () => handleMouseLeave(pElement));
|
|
|
|
|
+ setTimeout(() => {}, 50);
|
|
|
|
|
+ console.log(editorRef.value.selection, 9999);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 50);
|
|
|
};
|
|
};
|
|
|
|
|
+
|
|
|
const handleMouseLeave = (pElement) => {
|
|
const handleMouseLeave = (pElement) => {
|
|
|
const existingBtn = pElement.querySelector('.ai-btn');
|
|
const existingBtn = pElement.querySelector('.ai-btn');
|
|
|
- console.log(existingBtn, 9999);
|
|
|
|
|
if (existingBtn) {
|
|
if (existingBtn) {
|
|
|
pElement.removeChild(existingBtn);
|
|
pElement.removeChild(existingBtn);
|
|
|
}
|
|
}
|
|
@@ -185,32 +326,112 @@ const handleMouseLeave = (pElement) => {
|
|
|
const handleParentMouseLeave = (event, pTag) => {
|
|
const handleParentMouseLeave = (event, pTag) => {
|
|
|
const pElement = event.target;
|
|
const pElement = event.target;
|
|
|
pElement.style.background = '#fff';
|
|
pElement.style.background = '#fff';
|
|
|
- console.dir(pElement, 222);
|
|
|
|
|
handleMouseLeave(pElement);
|
|
handleMouseLeave(pElement);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const closeAIModal = () => {
|
|
const closeAIModal = () => {
|
|
|
- show_ai_modal.value = false
|
|
|
|
|
- target_active_pElement.value = null
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ show_ai_modal.value = false;
|
|
|
|
|
+ target_active_pElement.value = null;
|
|
|
|
|
+};
|
|
|
|
|
+// 需要删除的文本字数
|
|
|
|
|
+let offset = 0;
|
|
|
|
|
+// 修改替换内容的方法
|
|
|
const changePElement = (obj) => {
|
|
const changePElement = (obj) => {
|
|
|
- if (editorContentRef.value) {
|
|
|
|
|
- const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`)
|
|
|
|
|
|
|
+ if (editorRef.value) {
|
|
|
|
|
+ const editor = editorRef.value;
|
|
|
|
|
+ console.log(editor.selection);
|
|
|
|
|
+ const targetP = document.querySelector(`p#${obj.pid}`);
|
|
|
if (targetP) {
|
|
if (targetP) {
|
|
|
- targetP.innerHTML = obj.content.innerHTML
|
|
|
|
|
|
|
+ // 创建一个临时Range来选中整个p元素
|
|
|
|
|
+ const range = document.createRange();
|
|
|
|
|
+ range.selectNodeContents(targetP);
|
|
|
|
|
+
|
|
|
|
|
+ // 将DOM范围转换为编辑器选区
|
|
|
|
|
+ editor.restoreSelection(range);
|
|
|
|
|
+
|
|
|
|
|
+ offset = Number(editor.selection.focus.offset);
|
|
|
|
|
+ // 删除选中内容
|
|
|
|
|
+ for (let i = 0; i <= offset; i++) {
|
|
|
|
|
+ editor.deleteBackward();
|
|
|
|
|
+ }
|
|
|
|
|
+ // 插入空格
|
|
|
|
|
+ editor.insertBreak();
|
|
|
|
|
+ // 替换为新内容
|
|
|
|
|
+ editor.dangerouslyInsertHtml(obj.content.innerHTML);
|
|
|
|
|
+ // targetP.innerHTML = obj.content.innerHTML;
|
|
|
|
|
+ console.log(editor.getHtml());
|
|
|
}
|
|
}
|
|
|
|
|
+ // }
|
|
|
}
|
|
}
|
|
|
- closeAIModal()
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ closeAIModal();
|
|
|
|
|
+
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ bindPElementEvents();
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
const insertPElement = (obj) => {
|
|
const insertPElement = (obj) => {
|
|
|
if (editorContentRef.value) {
|
|
if (editorContentRef.value) {
|
|
|
- const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`)
|
|
|
|
|
|
|
+ const editor = editorRef.value;
|
|
|
|
|
+ const targetP = editorContentRef.value.$el.querySelector(`p#${obj.pid}`);
|
|
|
if (targetP) {
|
|
if (targetP) {
|
|
|
- targetP.innerHTML += obj.content.innerHTML
|
|
|
|
|
|
|
+ // targetP.innerHTML += obj.content.innerHTML;
|
|
|
|
|
+ editor.dangerouslyInsertHtml(obj.content.innerHTML);
|
|
|
|
|
+ editor.updateView();
|
|
|
|
|
+ console.log(editor.getHtml());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- closeAIModal()
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ closeAIModal();
|
|
|
|
|
+
|
|
|
|
|
+ // 插入后重新绑定事件
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ bindPElementEvents();
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 将图片转换为base64格式
|
|
|
|
|
+const convertImageToBase64 = (file) => {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ const reader = new FileReader();
|
|
|
|
|
+ reader.onload = (e) => {
|
|
|
|
|
+ resolve(e.target.result);
|
|
|
|
|
+ };
|
|
|
|
|
+ reader.onerror = (error) => {
|
|
|
|
|
+ reject(error);
|
|
|
|
|
+ };
|
|
|
|
|
+ reader.readAsDataURL(file);
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 处理图片上传
|
|
|
|
|
+const handleImageUpload = async (e) => {
|
|
|
|
|
+ const file = e.target.files[0];
|
|
|
|
|
+ if (!file) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const editor = editorRef.value;
|
|
|
|
|
+ if (!editor) {
|
|
|
|
|
+ console.error('编辑器实例不存在');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为base64
|
|
|
|
|
+ const base64Url = await convertImageToBase64(file);
|
|
|
|
|
+
|
|
|
|
|
+ // 插入图片到编辑器
|
|
|
|
|
+ editor.insertImage(base64Url, file.name);
|
|
|
|
|
+
|
|
|
|
|
+ // 图片插入后重新绑定事件
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ bindPElementEvents();
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('图片上传失败:', error);
|
|
|
|
|
+ alert('图片上传失败,请重试');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置input值,以便可以重复上传同一文件
|
|
|
|
|
+ e.target.value = '';
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
watch(
|
|
watch(
|
|
|
() => props.target_active_item,
|
|
() => props.target_active_item,
|
|
@@ -220,10 +441,18 @@ watch(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
);
|
|
);
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ getHtml: () => {
|
|
|
|
|
+ if (editorRef.value) {
|
|
|
|
|
+ return editorRef.value.getHtml();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
</script>
|
|
</script>
|
|
|
<style scoped lang="css">
|
|
<style scoped lang="css">
|
|
|
.editor {
|
|
.editor {
|
|
|
- /* flex: 4; */
|
|
|
|
|
width: calc(100%);
|
|
width: calc(100%);
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
@@ -238,28 +467,26 @@ watch(
|
|
|
.editor_content {
|
|
.editor_content {
|
|
|
scroll-margin-top: 80px;
|
|
scroll-margin-top: 80px;
|
|
|
height: calc(100% - 80px) !important;
|
|
height: calc(100% - 80px) !important;
|
|
|
- /* 留出Toolbar高度 */
|
|
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
padding: 0px 62px;
|
|
padding: 0px 62px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
:deep(.w-e-text-container .w-e-scroll) {
|
|
:deep(.w-e-text-container .w-e-scroll) {
|
|
|
- padding: 20px 50px;
|
|
|
|
|
|
|
+ padding: 20px 40px;
|
|
|
-ms-overflow-style: none;
|
|
-ms-overflow-style: none;
|
|
|
- scrollbar-width: none;
|
|
|
|
|
|
|
+ /* scrollbar-width: none; */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 为图片添加一些样式 */
|
|
|
|
|
+:deep(.w-e-image-container img) {
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ margin: 10px 0;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* .ai-btn {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: -30px;
|
|
|
|
|
- left: 0;
|
|
|
|
|
- padding: 5px 10px;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- background-image: url('../../assets/robot.svg');
|
|
|
|
|
- width: 30px;
|
|
|
|
|
- height: 30px;
|
|
|
|
|
|
|
+/* 确保AI按钮不会被编辑器遮挡 */
|
|
|
|
|
+:deep(.ai-btn) {
|
|
|
|
|
+ z-index: 10;
|
|
|
}
|
|
}
|
|
|
-.active_p {
|
|
|
|
|
- background-color: #007bff;
|
|
|
|
|
-} */
|
|
|
|
|
</style>
|
|
</style>
|