diff --git a/src/components.d.ts b/src/components.d.ts index 0b3820a..207249a 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -7,7 +7,6 @@ export {} declare module "vue" { export interface GlobalComponents { - Editor: typeof import("./components/Editor/index.vue")["default"]; ElAside: typeof import("element-plus/es")["ElAside"]; ElAutocomplete: typeof import("element-plus/es")["ElAutocomplete"]; ElBreadcrumb: typeof import("element-plus/es")["ElBreadcrumb"]; @@ -61,10 +60,6 @@ declare module "vue" { IEpRemove: typeof import("~icons/ep/remove")["default"]; IEpSearch: typeof import("~icons/ep/search")["default"]; IEpSwitchButton: typeof import("~icons/ep/switch-button")["default"]; - Index2: typeof import("./components/Editor/index2.vue")["default"]; - Index3333: typeof import("./components/Editor/index3333.vue")["default"]; - Index444: typeof import("./components/Editor/index444.vue")["default"]; - Index5555: typeof import("./components/Editor/index5555.vue")["default"]; RouterLink: typeof import("vue-router")["RouterLink"]; RouterView: typeof import("vue-router")["RouterView"]; } diff --git a/src/components/Editor/index.vue b/src/components/Editor/index.vue index 740bcbd..7af50fe 100644 --- a/src/components/Editor/index.vue +++ b/src/components/Editor/index.vue @@ -38,7 +38,14 @@
- + - - + + + + + - 取消 + 取消 确认
@@ -83,7 +119,7 @@ import { titleConfig } from "./titleConfig.js"; import { uploadVideo, uploadImg } from "@/api/modules/upload"; import { ElNotification } from "element-plus"; import { useRouter } from "vue-router"; - +import { useMsg } from "@/hooks/useMsg"; // 字体配置 let fontSizeStyle = Quill.import("attributors/style/size"); fontSizeStyle.whitelist = ["12px", "14px", "16px", "18px", "20px", "22px", "24px", "26px", "28px", "30px", "32px"]; @@ -108,13 +144,16 @@ const uploadFileVideo = ref(null); const outerVisible = ref(false); const imageList = ref([]); const imageListDb = ref([]); -const tabsData = ref([]); -const activeName = ref(null); +const activeName = ref(null); // 跟踪当前激活的标签页key const activeEditor = ref("main"); // 跟踪当前活跃编辑器:main/tab-索引 +// 标签页数据(新增key作为唯一标识,isEditing控制编辑状态) +const tabsData = ref([]); // 标签页编辑器ref数组 const tabEditors = ref([]); - +// 标题编辑输入框的ref +const editInputRefs = ref([]); +const currentEditingTabsRef = ref(null); // Props const props = defineProps({ content: { type: String, default: "" }, @@ -131,7 +170,7 @@ const editorContent = computed({ }); const myQuillEditor = ref(null); // 主编辑器ref -// 主编辑器配置 +// 主编辑器配置(保持不变) const options = reactive({ theme: "snow", debug: "warn", @@ -172,7 +211,7 @@ const options = reactive({ readOnly: props.readOnly }); -// 标签页编辑器配置 +// 标签页编辑器配置(保持不变) const options1 = reactive({ theme: "snow", debug: "warn", @@ -193,14 +232,14 @@ const options1 = reactive({ handlers: { image: function (value) { if (value) { - const currentIndex = tabsData.value.findIndex(item => item.title === activeName.value); + const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value); activeEditor.value = `tab-${currentIndex}`; proxy.$refs.uploadRef.click(); } else Quill.format("customImage", true); }, video: function (value) { if (value) { - const currentIndex = tabsData.value.findIndex(item => item.title === activeName.value); + const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value); activeEditor.value = `tab-${currentIndex}`; document.querySelector("#uploadFileVideo")?.click(); } else Quill.format("customVideo", true); @@ -212,7 +251,7 @@ const options1 = reactive({ readOnly: props.readOnly }); -// 上传前校验 +// 上传前校验(保持不变) const handleBeforeUpload = file => { const fileType = file.type; file.customUid = generateUUID(); @@ -234,7 +273,7 @@ const handleBeforeUpload = file => { return true; }; -// 图片上传(仅修复最后一张删除问题) +// 图片上传(保持不变) const handleHttpUpload = async options => { let formData = new FormData(); formData.append("image", options.file); @@ -246,14 +285,12 @@ const handleHttpUpload = async options => { const { data } = result.data; const { imgId } = result; - // 原有文件匹配逻辑保留 const fileItem = imageListDb.value.find(item => item.customUid === imgId); if (fileItem) { fileItem.serverImgId = imgId; fileItem.path = data.path; } - // 检查所有文件上传完成 const allFilesUploaded = imageListDb.value.every(item => item.path); if (allFilesUploaded) { let rawQuillEditor = ""; @@ -267,7 +304,6 @@ const handleHttpUpload = async options => { quill = rawQuillEditor.getQuill(); } - // 关键修复:插入后强制刷新编辑器选区 imageListDb.value.forEach(item => { const length = quill.getLength() - 1; quill.insertEmbed(length, "customImage", { @@ -277,9 +313,8 @@ const handleHttpUpload = async options => { quill.setSelection(length + 1); }); - // 修复:清空数组前先保存最后一个光标位置 const finalLength = quill.getLength(); - quill.setSelection(finalLength); // 确保光标在最后 + quill.setSelection(finalLength); imageList.value = []; imageListDb.value = []; @@ -287,11 +322,12 @@ const handleHttpUpload = async options => { } } catch (error) { console.error("图片上传失败:", error); - // 异常时清理当前文件 imageList.value = imageList.value.filter(item => item.customUid !== options.file.customUid); imageListDb.value = imageListDb.value.filter(item => item.customUid !== options.file.customUid); } }; + +// 视频上传(保持不变) const handleVideoUpload = async evt => { if (evt.target.files.length === 0) return; const formData = new FormData(); @@ -318,85 +354,131 @@ const handleVideoUpload = async evt => { console.log(error); } }; -// // 视频上传(区分tab和主富文本) -// const handleVideoUpload = async evt => { -// if (evt.target.files.length === 0) return; -// const formData = new FormData(); -// formData.append("video", evt.target.files[0]); -// try { -// const quill = await getCurrentQuillInstance(); -// if (!quill) return; - -// // 记录当前活跃编辑器类型,用于日志 -// const editorType = activeEditor.value === "main" ? "主编辑器" : `标签页${activeEditor.value.split("-")[1]}`; -// console.log(`视频将插入到: ${editorType}`); - -// const { data } = await uploadVideo(formData); -// const length = quill.selection.savedRange?.index || quill.getLength(); -// quill.insertEmbed(length, "customVideo", { -// url: h + data.path, -// id: generateUUID() -// }); -// evt.target.value = ""; -// } catch (error) { -// console.error("视频上传失败:", error); -// ElNotification({ title: "上传失败", message: "视频上传出错", type: "error" }); -// } -// }; - -// 标签页切换事件 -const handleTabChange = tabTitle => { - const tabIndex = tabsData.value.findIndex(item => item.title === tabTitle); +// 标签页切换事件(基于key切换) +const handleTabChange = key => { + const tabIndex = tabsData.value.findIndex(item => item.key === key); + activeName.value = key; activeEditor.value = `tab-${tabIndex}`; - console.log(`切换到标签页: ${tabTitle} (索引: ${tabIndex})`); }; -// 标签页编辑事件 -const handleTabsEdit = (targetName, action) => { - console.log(action, "===action===="); +// 标签页增删事件 +const handleTabsEdit = (targetKey, action) => { if (action === "add") { + if (tabsData.value.length > 5) { + return useMsg("error", "标签页已达上限 !"); + } + // 新增标签页:生成唯一key,默认标题,初始不处于编辑状态 + const newKey = `tab_${generateUUID()}`; const newIndex = tabsData.value.length; - tabsData.value.push({ title: `标签${newIndex + 1}`, content: "" }); + tabsData.value.push({ + key: newKey, + title: `标签${newIndex + 1}`, + content: "", + isEditing: false // 新增时默认不编辑 + }); nextTick(() => { - activeName.value = `标签${newIndex + 1}`; + activeName.value = newKey; activeEditor.value = `tab-${newIndex}`; + // 新增后自动进入编辑状态 + setTimeout(() => { + startEditTitle(newIndex); + }, 100); }); } else if (action === "remove") { - const index = tabsData.value.findIndex(item => item.title === targetName); + // 删除标签页 + const index = tabsData.value.findIndex(item => item.key === targetKey); tabsData.value.splice(index, 1); tabEditors.value.splice(index, 1); + editInputRefs.value.splice(index, 1); - // 修正 activeEditor 索引 + // 调整活跃编辑器索引 if (activeEditor.value.startsWith("tab-")) { const currentTabIndex = parseInt(activeEditor.value.split("-")[1]); if (currentTabIndex > index) { - // 若当前活跃标签页在删除项之后,索引减1 activeEditor.value = `tab-${currentTabIndex - 1}`; } else if (currentTabIndex === index) { - // 若删除的是当前活跃标签页,切换到第一个标签页 + // 若删除当前活跃标签,切换到第一个或主编辑器 activeEditor.value = tabsData.value.length > 0 ? "tab-0" : "main"; + activeName.value = tabsData.value.length > 0 ? tabsData.value[0].key : null; } } } }; -// 其他方法 +// 开始编辑标签页标题 +const startEditTitle = index => { + const tab = tabsData.value[index]; + if (!tab) return; + // 记录原始标题(用于取消编辑时恢复) + tab.originalTitle = tab.title; + tab.isEditing = true; + // 延迟获取焦点,确保输入框已渲染 + nextTick(() => { + editInputRefs.value[index]?.focus(); + }); +}; + +// 完成编辑(失去焦点或回车) +const finishEditTitle = index => { + const tab = tabsData.value[index]; + if (!tab) return; + // 校验标题(不能为空) + if (!tab.title.trim()) { + tab.title = tab.originalTitle || `标签${index + 1}`; + ElNotification({ title: "提示", message: "标签标题不能为空", type: "info" }); + } + tab.isEditing = false; + // 更新activeName(如果当前编辑的是活跃标签) + if (tab.key === activeName.value) { + activeName.value = tab.key; // 触发重绘 + } +}; + +// 其他方法(保持不变) const onContentChange = content => { emit("handleRichTextContentChange", content); emit("update:content", content); }; - +const setTabsInfo = () => { + outerVisible.value = false; + //清空 + tabsData.value = []; + activeName.value = null; + activeEditor.value = "main"; +}; +//弹窗关闭前的钩子 +const handleBeforeClose = () => { + setTabsInfo(); +}; +// 确认按钮点击事件(修改后) const handleQR = () => { const quill = toRaw(myQuillEditor.value)?.getQuill(); - if (quill) { - const range = quill.getSelection(true); + if (!quill) return; + if (!tabsData.value.length) { + return useMsg("error", "标签页内容为空 !"); + } + const range = quill.getSelection(true); + // 判断是否是编辑已有标签页(通过 currentEditingTabsRef 是否有值) + if (currentEditingTabsRef.value) { + // 1. 编辑模式:更新原有标签页组件 + const blot = currentEditingTabsRef.value; + // 更新 blot 的数据(触发 DOM 更新) + blot.updateContents(tabsData.value); // 需要在 TabsBlot 中添加 updateContents 方法 + // 清除编辑状态标记 + currentEditingTabsRef.value = null; + } else { + // 2. 新增模式:插入新的标签页组件 quill.insertEmbed(range.index, "tabs", tabsData.value); quill.setSelection(range.index + 1); - outerVisible.value = false; } + // 关闭弹窗并清空临时数据 + setTabsInfo(); +}; +//取消 +const handleQX = () => { + setTabsInfo(); }; - const initTitle = () => { const editor = document.querySelector(".ql-editor"); if (editor) editor.dataset.placeholder = ""; @@ -405,9 +487,49 @@ const initTitle = () => { if (tip) tip.setAttribute("title", item.title); }); }; +// 定义 loadTabsDataToEditor 函数 +const loadTabsDataToEditor = tabs => { + // 清空现有数据 + tabsData.value = []; + // 转换原始标签数据为编辑所需格式(添加key和编辑状态) + tabs.forEach((tab, index) => { + tabsData.value.push({ + key: `tab_${generateUUID()}`, // 生成唯一key + title: tab.title || `标签${index + 1}`, // 避免空标题 + content: tab.content || "", // 标签页内容 + isEditing: false // 编辑状态标记 + }); + }); + // 激活第一个标签页(如果有数据) + nextTick(() => { + if (tabsData.value.length > 0) { + activeName.value = tabsData.value[0].key; + activeEditor.value = "tab-0"; + } + }); +}; onMounted(() => { initTitle(); + // 监听编辑按钮点击事件 + const editorEl = document.querySelector(".ql-editor"); + if (editorEl) { + editorEl.addEventListener("edit-tabs", e => { + console.log(1232, "测试"); + console.log(e.detail.blot, "=e.detail="); + const tabsData = TabsBlot.value(e.detail.blot.domNode); + console.log(tabsData, "=tabsData="); + if (tabsData.length > 0) { + // 保存当前编辑的标签页引用 + currentEditingTabsRef.value = e.detail.blot; + console.log(currentEditingTabsRef.value, "=currentEditingTabsRef.value ="); + // 加载数据到弹窗 + loadTabsDataToEditor(tabsData); + // 显示弹窗 + outerVisible.value = true; + } + }); + } }); defineExpose({ @@ -420,6 +542,7 @@ defineExpose({ } }); + diff --git a/src/components/Editor/index2.vue b/src/components/Editor/index2.vue index 506b25d..7a2cec4 100644 --- a/src/components/Editor/index2.vue +++ b/src/components/Editor/index2.vue @@ -38,7 +38,14 @@
- + - - + + + + + - 取消 + 取消 确认
@@ -108,12 +136,15 @@ const uploadFileVideo = ref(null); const outerVisible = ref(false); const imageList = ref([]); const imageListDb = ref([]); -const tabsData = ref([]); -const activeName = ref(null); +const activeName = ref(null); // 跟踪当前激活的标签页key const activeEditor = ref("main"); // 跟踪当前活跃编辑器:main/tab-索引 +// 标签页数据(新增key作为唯一标识,isEditing控制编辑状态) +const tabsData = ref([]); // 标签页编辑器ref数组 const tabEditors = ref([]); +// 标题编辑输入框的ref +const editInputRefs = ref([]); // Props const props = defineProps({ @@ -131,7 +162,7 @@ const editorContent = computed({ }); const myQuillEditor = ref(null); // 主编辑器ref -// 主编辑器配置 +// 主编辑器配置(保持不变) const options = reactive({ theme: "snow", debug: "warn", @@ -172,7 +203,7 @@ const options = reactive({ readOnly: props.readOnly }); -// 标签页编辑器配置 +// 标签页编辑器配置(保持不变) const options1 = reactive({ theme: "snow", debug: "warn", @@ -193,14 +224,14 @@ const options1 = reactive({ handlers: { image: function (value) { if (value) { - const currentIndex = tabsData.value.findIndex(item => item.title === activeName.value); + const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value); activeEditor.value = `tab-${currentIndex}`; proxy.$refs.uploadRef.click(); } else Quill.format("customImage", true); }, video: function (value) { if (value) { - const currentIndex = tabsData.value.findIndex(item => item.title === activeName.value); + const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value); activeEditor.value = `tab-${currentIndex}`; document.querySelector("#uploadFileVideo")?.click(); } else Quill.format("customVideo", true); @@ -212,7 +243,7 @@ const options1 = reactive({ readOnly: props.readOnly }); -// 上传前校验 +// 上传前校验(保持不变) const handleBeforeUpload = file => { const fileType = file.type; file.customUid = generateUUID(); @@ -234,7 +265,7 @@ const handleBeforeUpload = file => { return true; }; -// 图片上传(仅修复最后一张删除问题) +// 图片上传(保持不变) const handleHttpUpload = async options => { let formData = new FormData(); formData.append("image", options.file); @@ -246,14 +277,12 @@ const handleHttpUpload = async options => { const { data } = result.data; const { imgId } = result; - // 原有文件匹配逻辑保留 const fileItem = imageListDb.value.find(item => item.customUid === imgId); if (fileItem) { fileItem.serverImgId = imgId; fileItem.path = data.path; } - // 检查所有文件上传完成 const allFilesUploaded = imageListDb.value.every(item => item.path); if (allFilesUploaded) { let rawQuillEditor = ""; @@ -267,7 +296,6 @@ const handleHttpUpload = async options => { quill = rawQuillEditor.getQuill(); } - // 关键修复:插入后强制刷新编辑器选区 imageListDb.value.forEach(item => { const length = quill.getLength() - 1; quill.insertEmbed(length, "customImage", { @@ -277,9 +305,8 @@ const handleHttpUpload = async options => { quill.setSelection(length + 1); }); - // 修复:清空数组前先保存最后一个光标位置 const finalLength = quill.getLength(); - quill.setSelection(finalLength); // 确保光标在最后 + quill.setSelection(finalLength); imageList.value = []; imageListDb.value = []; @@ -287,11 +314,12 @@ const handleHttpUpload = async options => { } } catch (error) { console.error("图片上传失败:", error); - // 异常时清理当前文件 imageList.value = imageList.value.filter(item => item.customUid !== options.file.customUid); imageListDb.value = imageListDb.value.filter(item => item.customUid !== options.file.customUid); } }; + +// 视频上传(保持不变) const handleVideoUpload = async evt => { if (evt.target.files.length === 0) return; const formData = new FormData(); @@ -318,84 +346,114 @@ const handleVideoUpload = async evt => { console.log(error); } }; -// // 视频上传(区分tab和主富文本) -// const handleVideoUpload = async evt => { -// if (evt.target.files.length === 0) return; -// const formData = new FormData(); -// formData.append("video", evt.target.files[0]); -// try { -// const quill = await getCurrentQuillInstance(); -// if (!quill) return; - -// // 记录当前活跃编辑器类型,用于日志 -// const editorType = activeEditor.value === "main" ? "主编辑器" : `标签页${activeEditor.value.split("-")[1]}`; -// console.log(`视频将插入到: ${editorType}`); - -// const { data } = await uploadVideo(formData); -// const length = quill.selection.savedRange?.index || quill.getLength(); -// quill.insertEmbed(length, "customVideo", { -// url: h + data.path, -// id: generateUUID() -// }); -// evt.target.value = ""; -// } catch (error) { -// console.error("视频上传失败:", error); -// ElNotification({ title: "上传失败", message: "视频上传出错", type: "error" }); -// } -// }; - -// 标签页切换事件 -const handleTabChange = tabTitle => { - const tabIndex = tabsData.value.findIndex(item => item.title === tabTitle); +// 标签页切换事件(基于key切换) +const handleTabChange = key => { + const tabIndex = tabsData.value.findIndex(item => item.key === key); + activeName.value = key; activeEditor.value = `tab-${tabIndex}`; - console.log(`切换到标签页: ${tabTitle} (索引: ${tabIndex})`); }; -// 标签页编辑事件 -const handleTabsEdit = (targetName, action) => { +// 标签页增删事件 +const handleTabsEdit = (targetKey, action) => { if (action === "add") { + // 新增标签页:生成唯一key,默认标题,初始不处于编辑状态 + const newKey = `tab_${generateUUID()}`; const newIndex = tabsData.value.length; - tabsData.value.push({ title: `标签${newIndex + 1}`, content: "" }); + tabsData.value.push({ + key: newKey, + title: `标签${newIndex + 1}`, + content: "", + isEditing: false // 新增时默认不编辑 + }); nextTick(() => { - activeName.value = `标签${newIndex + 1}`; + activeName.value = newKey; activeEditor.value = `tab-${newIndex}`; + // 新增后自动进入编辑状态 + setTimeout(() => { + startEditTitle(newIndex); + }, 100); }); } else if (action === "remove") { - const index = tabsData.value.findIndex(item => item.title === targetName); + // 删除标签页 + const index = tabsData.value.findIndex(item => item.key === targetKey); tabsData.value.splice(index, 1); tabEditors.value.splice(index, 1); + editInputRefs.value.splice(index, 1); - // 修正 activeEditor 索引 + // 调整活跃编辑器索引 if (activeEditor.value.startsWith("tab-")) { const currentTabIndex = parseInt(activeEditor.value.split("-")[1]); if (currentTabIndex > index) { - // 若当前活跃标签页在删除项之后,索引减1 activeEditor.value = `tab-${currentTabIndex - 1}`; } else if (currentTabIndex === index) { - // 若删除的是当前活跃标签页,切换到第一个标签页 + // 若删除当前活跃标签,切换到第一个或主编辑器 activeEditor.value = tabsData.value.length > 0 ? "tab-0" : "main"; + activeName.value = tabsData.value.length > 0 ? tabsData.value[0].key : null; } } } }; -// 其他方法 +// 开始编辑标签页标题 +const startEditTitle = index => { + const tab = tabsData.value[index]; + if (!tab) return; + // 记录原始标题(用于取消编辑时恢复) + tab.originalTitle = tab.title; + tab.isEditing = true; + // 延迟获取焦点,确保输入框已渲染 + nextTick(() => { + editInputRefs.value[index]?.focus(); + }); +}; + +// 完成编辑(失去焦点或回车) +const finishEditTitle = index => { + const tab = tabsData.value[index]; + if (!tab) return; + // 校验标题(不能为空) + if (!tab.title.trim()) { + tab.title = tab.originalTitle || `标签${index + 1}`; + ElNotification({ title: "提示", message: "标签标题不能为空", type: "info" }); + } + tab.isEditing = false; + // 更新activeName(如果当前编辑的是活跃标签) + if (tab.key === activeName.value) { + activeName.value = tab.key; // 触发重绘 + } +}; + +// 其他方法(保持不变) const onContentChange = content => { emit("handleRichTextContentChange", content); emit("update:content", content); }; - +const setTabsInfo = () => { + outerVisible.value = false; + //清空 + tabsData.value = []; + activeName.value = null; + activeEditor.value = "main"; +}; +//弹窗关闭前的钩子 +const handleBeforeClose = () => { + setTabsInfo(); +}; +//确认 const handleQR = () => { const quill = toRaw(myQuillEditor.value)?.getQuill(); if (quill) { const range = quill.getSelection(true); quill.insertEmbed(range.index, "tabs", tabsData.value); quill.setSelection(range.index + 1); - outerVisible.value = false; + setTabsInfo(); } }; - +//取消 +const handleQX = () => { + setTabsInfo(); +}; const initTitle = () => { const editor = document.querySelector(".ql-editor"); if (editor) editor.dataset.placeholder = ""; @@ -419,6 +477,7 @@ defineExpose({ } }); + diff --git a/src/components/Editor/index3333.vue b/src/components/Editor/index3333.vue deleted file mode 100644 index ff28764..0000000 --- a/src/components/Editor/index3333.vue +++ /dev/null @@ -1,370 +0,0 @@ - - - - - diff --git a/src/components/Editor/index444.vue b/src/components/Editor/index444.vue deleted file mode 100644 index 6640f73..0000000 --- a/src/components/Editor/index444.vue +++ /dev/null @@ -1,446 +0,0 @@ - - - - - diff --git a/src/components/Editor/index5555.vue b/src/components/Editor/index5555.vue deleted file mode 100644 index 5ad6288..0000000 --- a/src/components/Editor/index5555.vue +++ /dev/null @@ -1,517 +0,0 @@ - - - - - diff --git a/src/components/Editor/quill-tabs.js b/src/components/Editor/quill-tabs.js index da6ec9c..8cd45c1 100644 --- a/src/components/Editor/quill-tabs.js +++ b/src/components/Editor/quill-tabs.js @@ -5,79 +5,370 @@ const BlockEmbed = Quill.import("blots/block/embed"); class TabsBlot extends BlockEmbed { static blotName = "tabs"; static tagName = "div"; - static className = "quill-tabs"; + static className = "m-quill-tabs"; constructor(domNode) { super(domNode); this.bindEvents(); + this.bindDeleteKeyEvent(); // 绑定删除键事件 } static create(value) { const node = super.create(value); - const tabs = value; + const tabs = value || []; + + // 主容器样式 + node.setAttribute( + "style", + ` + margin: 15px 0; + overflow: hidden; + border-radius: 4px; + position: relative; + ` + ); + // 标签栏 const tabList = document.createElement("div"); - tabList.className = "quill-tab-list"; + tabList.className = "m-quill-tab-list"; + tabList.setAttribute( + "style", + ` + display: flex; + border-bottom: 1px solid #dddddd; + ` + ); + // 内容区 const contentList = document.createElement("div"); - contentList.className = "quill-tab-content-list"; - // 生成标签和内容 + contentList.className = "m-quill-tab-content-list"; + contentList.setAttribute( + "style", + ` + padding: 15px; + ` + ); + + // 生成标签按钮和内容面板 tabs.forEach((tab, index) => { // 标签按钮 const btn = document.createElement("button"); - btn.className = `quill-tab-button ${index === 0 ? "active" : ""}`; + btn.className = `m-quill-tab-button`; btn.setAttribute("data-index", index); btn.textContent = tab.title; + btn.setAttribute( + "style", + ` + padding: 10px 15px; + font-weight: 900; + color: #8f9099; + cursor: pointer; + background: transparent; + border: none; + margin-right: 60px; + cursor: pointer; + font-size:16px; + ${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""} + ` + ); tabList.appendChild(btn); - // 内容面板 + // 内容面板 - 关键修改:设置为可编辑 const panel = document.createElement("div"); - panel.className = `quill-tab-content ${index === 0 ? "active" : ""}`; + panel.className = `m-quill-tab-content`; panel.setAttribute("data-index", index); panel.innerHTML = tab.content; + panel.setAttribute( + "style", + ` + display: ${index === 0 ? "block" : "none"}; + min-height: 50px; + ` + ); + panel.contentEditable = "false"; // 内容面板可编辑 contentList.appendChild(panel); }); + // 编辑按钮 + const editBtn = document.createElement("button"); + editBtn.className = "m-quill-tab-edit-btn"; + editBtn.innerHTML = "编辑"; + editBtn.setAttribute("data-action", "edit"); + editBtn.setAttribute( + "style", + ` + + padding: 10px; + margin-left: auto; + color: #606266; + cursor: pointer; + background: transparent; + border: none; + display:block; + ` + ); + // display: flex; + //align-items: center; + // editBtn.onmouseover = () => { + // editBtn.style.color = "#007bff"; + // }; + // editBtn.onmouseout = () => { + // editBtn.style.color = "#606266"; + // }; + tabList.appendChild(editBtn); + + // 标签页切换逻辑 + const scriptTag = document.createElement("script"); + // 修改 scriptTag.textContent 中的逻辑 + scriptTag.textContent = ` + (function() { + const container = document.currentScript.parentElement; + const isAdmin = window.location.pathname.includes('/admin'); + const editBtn1 = container.querySelector('.m-quill-tab-edit-btn'); + + // 仅在非管理系统(文章网站)隐藏编辑按钮,管理系统保持显示 + if (!isAdmin && editBtn1) { + editBtn1.style.display = 'none'; // 文章网站隐藏按钮 + } else if (isAdmin && editBtn1) { + editBtn1.style.display = 'block'; // 管理系统强制显示按钮 + } + + // 非管理系统才执行标签切换逻辑(管理系统不执行) + if (!isAdmin) { + const tabButtons = container.querySelectorAll('.m-quill-tab-button:not([data-action])'); + const contentPanels = container.querySelectorAll('.m-quill-tab-content'); + tabButtons.forEach(btn => { + btn.addEventListener('click', function() { + const index = parseInt(this.dataset.index); + tabButtons.forEach((b, i) => { + b.setAttribute('style', \` + padding: 10px 15px; + font-weight: 900; + cursor: pointer; + background: transparent; + font-size:16px; + margin-right: 60px; + color: #8f9099; + border: none; + \${i === index ? + 'color: #1f2635;border-bottom: 3px solid #537CD8;font-size:16px;' : + '' + } + \`); + }); + contentPanels.forEach((panel, i) => { + panel.style.display = i === index ? 'block' : 'none'; + }); + }); + }); + } + })(); + `; + node.appendChild(tabList); node.appendChild(contentList); - node.setAttribute("contenteditable", "false"); // 禁止直接编辑容器 + node.appendChild(scriptTag); + node.setAttribute("contenteditable", "false"); return node; } bindEvents() { - // 事件委托,确保动态生成的元素也能触发 - this.domNode.addEventListener("click", e => { - const btn = e.target.closest(".quill-tab-button"); - if (btn) { + if (!this.eventBoundElements) { + this.eventBoundElements = new WeakMap(); + } + + // 移除 eventBoundElements 依赖,直接重新绑定事件(避免弱映射导致的问题) + const editBtn = this.domNode.querySelector(".m-quill-tab-edit-btn"); + if (editBtn) { + // 先移除旧事件,避免重复绑定 + editBtn.removeEventListener("click", this.handleEditClick); + // 绑定新事件(使用箭头函数确保 this 指向正确) + this.handleEditClick = e => { e.stopPropagation(); - const index = parseInt(btn.dataset.index, 10); - this.selectTab(index); + console.log("1232323"); + this.domNode.dispatchEvent( + new CustomEvent("edit-tabs", { + bubbles: true, + detail: { blot: this } + }) + ); + }; + editBtn.addEventListener("click", this.handleEditClick); + } + + // 标签切换事件 + const tabButtons = this.domNode.querySelectorAll(".m-quill-tab-button:not([data-action])"); + tabButtons.forEach(btn => { + if (!this.eventBoundElements.has(btn)) { + btn.addEventListener("click", () => { + const index = parseInt(btn.dataset.index, 10); + this.selectTab(index); + }); + this.eventBoundElements.set(btn, true); } }); } - selectTab(index) { - const buttons = this.domNode.querySelectorAll(".quill-tab-button"); - const panels = this.domNode.querySelectorAll(".quill-tab-content"); + // 增强版删除键处理 + bindDeleteKeyEvent() { + // 阻止从外部删除整个组件 + this.domNode.addEventListener( + "keydown", + e => { + if (e.key === "Backspace" || e.key === "Delete") { + const selection = window.getSelection(); + if (!selection.rangeCount) return; - buttons.forEach((btn, i) => btn.classList.toggle("active", i === index)); - panels.forEach((panel, i) => panel.classList.toggle("active", i === index)); + const range = selection.getRangeAt(0); + const parentBlock = this.domNode; + + // 判断光标是否在组件内部 + const isInside = parentBlock.contains(range.commonAncestorContainer); + + // 如果光标在组件外部,阻止删除 + if (!isInside) { + e.preventDefault(); + return; + } + + // 检查是否选中了整个组件 + if ( + range.startContainer === parentBlock && + range.endContainer === parentBlock && + range.startOffset === 0 && + range.endOffset >= parentBlock.childNodes.length + ) { + e.preventDefault(); // 阻止删除整个组件 + } + } + }, + true + ); // 使用捕获阶段 + + // 标签栏禁止编辑 + const tabList = this.domNode.querySelector(".m-quill-tab-list"); + if (tabList) { + tabList.querySelectorAll("*").forEach(el => { + el.contentEditable = "false"; + }); + } + } + + selectTab(index) { + const buttons = this.domNode.querySelectorAll(".m-quill-tab-button:not([data-action])"); + const panels = this.domNode.querySelectorAll(".m-quill-tab-content"); + + buttons.forEach((btn, i) => { + btn.setAttribute( + "style", + ` + padding: 10px 15px; + font-weight: 900; + cursor: pointer; + background: transparent; + border: none; + font-size:16px; + margin-right: 60px; + color: #8f9099; + border-bottom: 3px solid transparent; + ${i === index ? "color: #1f2635;border-bottom: 3px solid #537CD8;font-size:16px;" : ""} + ` + ); + }); + + panels.forEach((panel, i) => { + panel.style.display = i === index ? "block" : "none"; + }); } static value(node) { const tabs = []; - node.querySelectorAll(".quill-tab-button").forEach((btn, i) => { + const buttons = node.querySelectorAll(".m-quill-tab-button:not([data-action])"); + const panels = node.querySelectorAll(".m-quill-tab-content"); + + buttons.forEach((btn, i) => { tabs.push({ title: btn.textContent, - content: node.querySelectorAll(".quill-tab-content")[i].innerHTML + content: panels[i]?.innerHTML || "" }); }); - return { tabs }; + + return tabs; } update(mutations, context) { super.update(mutations, context); - this.bindEvents(); // 重新绑定事件 + const scriptTag = this.domNode.querySelector("script"); + if (scriptTag) { + const newScript = document.createElement("script"); + + newScript.textContent = scriptTag.textContent; + scriptTag.parentNode.replaceChild(newScript, scriptTag); + } + this.bindEvents(); + this.bindDeleteKeyEvent(); // 重新绑定删除事件 + } + + getValue() { + return TabsBlot.value(this.domNode); + } + + // 更新标签页数据(编辑后更新 DOM) + updateContents(tabs) { + // 清空原有内容 + const contentList = this.domNode.querySelector(".m-quill-tab-content-list"); + const tabList = this.domNode.querySelector(".m-quill-tab-list"); + // 保留编辑按钮(如果需要) + const editBtn = this.domNode.querySelector(".m-quill-tab-edit-btn"); + // 清空标签栏和内容区(保留编辑按钮) + Array.from(tabList.children).forEach(child => { + if (!child.classList.contains("m-quill-tab-edit-btn")) { + child.remove(); + } + }); + contentList.innerHTML = ""; + + // 重新渲染标签页(复用 create 方法中的逻辑) + tabs.forEach((tab, index) => { + // 重建标签按钮 + const btn = document.createElement("button"); + btn.className = "m-quill-tab-button"; + btn.setAttribute("data-index", index); + btn.textContent = tab.title; + btn.setAttribute( + "style", + ` + padding: 10px 15px; + font-weight: 900; + color: #8f9099; + cursor: pointer; + background: transparent; + border: none; + margin-right: 60px; + font-size:16px; + ${index === 0 ? "color: #1f2635; border-bottom: 3px solid #537CD8;font-size:16px;" : ""} + ` + ); + tabList.insertBefore(btn, editBtn); // 插入到编辑按钮前 + + // 重建内容面板 + const panel = document.createElement("div"); + panel.className = "m-quill-tab-content"; + panel.setAttribute("data-index", index); + panel.innerHTML = tab.content; + panel.setAttribute( + "style", + ` + display: ${index === 0 ? "block" : "none"}; + min-height: 50px; + ` + ); + panel.contentEditable = "false"; + contentList.appendChild(panel); + }); + + // 重新绑定事件(确保切换功能正常) + this.bindEvents(); } } diff --git a/src/components/Editor/quill-tabs1.js b/src/components/Editor/quill-tabs1.js index da6ec9c..814269f 100644 --- a/src/components/Editor/quill-tabs1.js +++ b/src/components/Editor/quill-tabs1.js @@ -10,74 +10,277 @@ class TabsBlot extends BlockEmbed { constructor(domNode) { super(domNode); this.bindEvents(); + this.bindDeleteKeyEvent(); // 绑定删除键事件 } static create(value) { const node = super.create(value); - const tabs = value; + const tabs = value || []; + + // 主容器样式 + node.setAttribute( + "style", + ` + margin: 15px 0; + overflow: hidden; + border: 1px solid #dddddd; + border-radius: 4px; + position: relative; + ` + ); + // 标签栏 const tabList = document.createElement("div"); tabList.className = "quill-tab-list"; + tabList.setAttribute( + "style", + ` + display: flex; + background-color: #f8f9fa; + border-bottom: 1px solid #dddddd; + ` + ); + // 内容区 const contentList = document.createElement("div"); contentList.className = "quill-tab-content-list"; - // 生成标签和内容 + contentList.setAttribute( + "style", + ` + padding: 15px; + ` + ); + + // 生成标签按钮和内容面板 tabs.forEach((tab, index) => { // 标签按钮 const btn = document.createElement("button"); btn.className = `quill-tab-button ${index === 0 ? "active" : ""}`; btn.setAttribute("data-index", index); btn.textContent = tab.title; + btn.setAttribute( + "style", + ` + padding: 10px 15px; + font-weight: 500; + cursor: pointer; + background: transparent; + border: none; + transition: background-color 0.2s; + ${index === 0 ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""} + ` + ); tabList.appendChild(btn); - // 内容面板 + // 内容面板 - 关键修改:设置为可编辑 const panel = document.createElement("div"); panel.className = `quill-tab-content ${index === 0 ? "active" : ""}`; panel.setAttribute("data-index", index); panel.innerHTML = tab.content; + panel.setAttribute( + "style", + ` + display: ${index === 0 ? "block" : "none"}; + min-height: 50px; /* 确保有编辑区域 */ + ` + ); + panel.contentEditable = "true"; // 内容面板可编辑 contentList.appendChild(panel); }); + // 编辑按钮 + const editBtn = document.createElement("button"); + editBtn.className = "quill-tab-edit-btn"; + editBtn.innerHTML = "编辑"; + editBtn.setAttribute("data-action", "edit"); + editBtn.setAttribute( + "style", + ` + display: flex; + align-items: center; + padding: 10px; + margin-left: auto; + color: #606266; + cursor: pointer; + background: transparent; + border: none; + ` + ); + editBtn.onmouseover = () => { + editBtn.style.color = "#007bff"; + }; + editBtn.onmouseout = () => { + editBtn.style.color = "#606266"; + }; + tabList.appendChild(editBtn); + // 标签页切换逻辑 + const scriptTag = document.createElement("script"); + scriptTag.textContent = ` + (function() { + const container = document.currentScript.parentElement; + const tabButtons = container.querySelectorAll('.quill-tab-button:not([data-action])'); + const contentPanels = container.querySelectorAll('.quill-tab-content'); + tabButtons.forEach(btn => { + btn.addEventListener('click', function() { + const index = parseInt(this.dataset.index); + tabButtons.forEach((b, i) => { + b.setAttribute('style', \` + padding: 10px 15px; + font-weight: 500; + cursor: pointer; + background: transparent; + border: none; + transition: background-color 0.2s; + \${i === index ? + 'color: #007bff; background-color: white; border-bottom: 2px solid #007bff;' : + '' + } + \`); + }); + + contentPanels.forEach((panel, i) => { + panel.style.display = i === index ? 'block' : 'none'; + }); + }); + }); + })(); + `; + + // 组装结构 - 关键修改:移除contenteditable="false" node.appendChild(tabList); node.appendChild(contentList); - node.setAttribute("contenteditable", "false"); // 禁止直接编辑容器 + node.appendChild(scriptTag); + return node; } bindEvents() { - // 事件委托,确保动态生成的元素也能触发 - this.domNode.addEventListener("click", e => { - const btn = e.target.closest(".quill-tab-button"); - if (btn) { + if (!this.eventBoundElements) { + this.eventBoundElements = new WeakMap(); + } + + // 编辑按钮事件 + const editBtn = this.domNode.querySelector(".quill-tab-edit-btn"); + if (editBtn && !this.eventBoundElements.has(editBtn)) { + editBtn.addEventListener("click", e => { e.stopPropagation(); - const index = parseInt(btn.dataset.index, 10); - this.selectTab(index); + this.domNode.dispatchEvent( + new CustomEvent("edit-tabs", { + bubbles: true, + detail: { blot: this } + }) + ); + }); + this.eventBoundElements.set(editBtn, true); + } + + // 标签切换事件 + const tabButtons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])"); + tabButtons.forEach(btn => { + if (!this.eventBoundElements.has(btn)) { + btn.addEventListener("click", () => { + const index = parseInt(btn.dataset.index, 10); + this.selectTab(index); + }); + this.eventBoundElements.set(btn, true); } }); } + // 增强版删除键处理 + bindDeleteKeyEvent() { + // 阻止从外部删除整个组件 + this.domNode.addEventListener( + "keydown", + e => { + if (e.key === "Backspace" || e.key === "Delete") { + const selection = window.getSelection(); + if (!selection.rangeCount) return; + + const range = selection.getRangeAt(0); + const parentBlock = this.domNode; + + // 判断光标是否在组件内部 + const isInside = parentBlock.contains(range.commonAncestorContainer); + + // 如果光标在组件外部,阻止删除 + if (!isInside) { + e.preventDefault(); + return; + } + + // 检查是否选中了整个组件 + if ( + range.startContainer === parentBlock && + range.endContainer === parentBlock && + range.startOffset === 0 && + range.endOffset >= parentBlock.childNodes.length + ) { + e.preventDefault(); // 阻止删除整个组件 + } + } + }, + true + ); // 使用捕获阶段 + + // 标签栏禁止编辑 + const tabList = this.domNode.querySelector(".quill-tab-list"); + if (tabList) { + tabList.querySelectorAll("*").forEach(el => { + el.contentEditable = "false"; + }); + } + } + selectTab(index) { - const buttons = this.domNode.querySelectorAll(".quill-tab-button"); + const buttons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])"); const panels = this.domNode.querySelectorAll(".quill-tab-content"); - buttons.forEach((btn, i) => btn.classList.toggle("active", i === index)); - panels.forEach((panel, i) => panel.classList.toggle("active", i === index)); + buttons.forEach((btn, i) => { + btn.setAttribute( + "style", + ` + padding: 10px 15px; + font-weight: 500; + cursor: pointer; + background: transparent; + border: none; + transition: background-color 0.2s; + ${i === index ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""} + ` + ); + }); + + panels.forEach((panel, i) => { + panel.style.display = i === index ? "block" : "none"; + }); } static value(node) { const tabs = []; - node.querySelectorAll(".quill-tab-button").forEach((btn, i) => { + const buttons = node.querySelectorAll(".quill-tab-button:not([data-action])"); + const panels = node.querySelectorAll(".quill-tab-content"); + + buttons.forEach((btn, i) => { tabs.push({ title: btn.textContent, - content: node.querySelectorAll(".quill-tab-content")[i].innerHTML + content: panels[i].innerHTML }); }); + return { tabs }; } update(mutations, context) { super.update(mutations, context); - this.bindEvents(); // 重新绑定事件 + const scriptTag = this.domNode.querySelector("script"); + if (scriptTag) { + const newScript = document.createElement("script"); + newScript.textContent = scriptTag.textContent; + scriptTag.parentNode.replaceChild(newScript, scriptTag); + } + this.bindEvents(); + this.bindDeleteKeyEvent(); // 重新绑定删除事件 } } diff --git a/src/components/Editor/quill-tabs222.js b/src/components/Editor/quill-tabs222.js new file mode 100644 index 0000000..eb80593 --- /dev/null +++ b/src/components/Editor/quill-tabs222.js @@ -0,0 +1,180 @@ +import { Quill } from "@vueup/vue-quill"; + +const BlockEmbed = Quill.import("blots/block/embed"); + +class TabsBlot extends BlockEmbed { + static blotName = "tabs"; + static tagName = "div"; + static className = "quill-tabs"; + + constructor(domNode) { + super(domNode); + this.bindEvents(); + } + + static create(value) { + const tabs = Array.isArray(value) ? value : []; + const node = super.create(value); + + // 主容器样式 + node.setAttribute( + "style", + ` + margin: 15px 0; + overflow: hidden; + border: 1px solid #dddddd; + border-radius: 4px; + position: relative; + ` + ); + + // 标签栏容器 + const tabList = document.createElement("div"); + tabList.className = "quill-tab-list"; + tabList.style.cssText = ` + display: flex; + background-color: #f8f9fa; + border-bottom: 1px solid #dddddd; + `; + + // 内容区容器 + const contentList = document.createElement("div"); + contentList.className = "quill-tab-content-list"; + contentList.style.cssText = "padding: 15px;"; + + // 生成标签和内容 + tabs.forEach((tab, index) => { + // 标签按钮 + const btn = document.createElement("button"); + btn.className = `quill-tab-button ${index === 0 ? "active" : ""}`; + btn.dataset.index = index; + btn.textContent = tab.title || `标签${index + 1}`; + btn.style.cssText = ` + padding: 10px 15px; + font-weight: 500; + cursor: pointer; + background: transparent; + border: none; + ${index === 0 ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""} + `; + tabList.appendChild(btn); + + // 内容面板 + const panel = document.createElement("div"); + panel.className = `quill-tab-content ${index === 0 ? "active" : ""}`; + panel.dataset.index = index; + panel.innerHTML = tab.content || ""; + panel.style.display = index === 0 ? "block" : "none"; + contentList.appendChild(panel); + }); + + // 编辑按钮 + const editBtn = document.createElement("button"); + editBtn.className = "quill-tab-edit-btn"; + editBtn.textContent = "编辑"; + editBtn.dataset.action = "edit"; + editBtn.style.cssText = ` + display: flex; + align-items: center; + padding: 10px; + margin-left: auto; + color: #606266; + cursor: pointer; + background: transparent; + border: none; + `; + editBtn.onmouseover = () => (editBtn.style.color = "#007bff"); + editBtn.onmouseout = () => (editBtn.style.color = "#606266"); + tabList.appendChild(editBtn); + + // 组装DOM + node.appendChild(tabList); + node.appendChild(contentList); + node.contentEditable = "false"; + + return node; + } + + // 改进的事件绑定方法,避免重复绑定 + bindEvents() { + // 使用WeakMap存储已绑定的元素,避免重复绑定 + if (!this.eventBoundElements) { + this.eventBoundElements = new WeakMap(); + } + + // 编辑按钮事件 + const editBtn = this.domNode.querySelector(".quill-tab-edit-btn"); + if (editBtn && !this.eventBoundElements.has(editBtn)) { + editBtn.addEventListener("click", e => { + e.stopPropagation(); + this.domNode.dispatchEvent( + new CustomEvent("edit-tabs", { + bubbles: true, + detail: { blot: this } + }) + ); + }); + this.eventBoundElements.set(editBtn, true); + } + + // 标签切换事件 + const tabButtons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])"); + tabButtons.forEach(btn => { + if (!this.eventBoundElements.has(btn)) { + btn.addEventListener("click", () => { + const index = parseInt(btn.dataset.index, 10); + this.selectTab(index); + }); + this.eventBoundElements.set(btn, true); + } + }); + } + + // 移除了有问题的unbindEvents方法 + + selectTab(index) { + const buttons = this.domNode.querySelectorAll(".quill-tab-button:not([data-action])"); + const panels = this.domNode.querySelectorAll(".quill-tab-content"); + + if (index < 0 || index >= buttons.length) return; + + // 更新按钮样式 + buttons.forEach((btn, i) => { + btn.style.cssText = ` + padding: 10px 15px; + font-weight: 500; + cursor: pointer; + background: transparent; + border: none; + ${i === index ? "color: #007bff; background-color: white; border-bottom: 2px solid #007bff;" : ""} + `; + }); + + // 更新内容面板显示 + panels.forEach((panel, i) => { + panel.style.display = i === index ? "block" : "none"; + }); + } + + static value(node) { + const tabs = []; + const buttons = node.querySelectorAll(".quill-tab-button:not([data-action])"); + const panels = node.querySelectorAll(".quill-tab-content"); + + buttons.forEach((btn, i) => { + tabs.push({ + title: btn.textContent, + content: panels[i]?.innerHTML || "" + }); + }); + + return tabs; + } + + update(mutations, context) { + super.update(mutations, context); + this.bindEvents(); + } +} + +export default TabsBlot;