883 lines
30 KiB
Vue
883 lines
30 KiB
Vue
<template>
|
||
<!-- 图片上传组件 -->
|
||
<el-upload
|
||
:id="uuid"
|
||
action="#"
|
||
:multiple="true"
|
||
:show-file-list="false"
|
||
:http-request="handleHttpUpload"
|
||
:before-upload="handleBeforeUpload"
|
||
class="editor-img-uploader"
|
||
accept=".jpeg,.jpg,.png,.gif"
|
||
>
|
||
<i ref="uploadRef" class="Plus editor-img-uploader"></i>
|
||
</el-upload>
|
||
|
||
<!-- 视频上传组件 -->
|
||
<input
|
||
type="file"
|
||
accept="video/*"
|
||
name="file"
|
||
ref="uploadFileVideo"
|
||
id="uploadFileVideo"
|
||
@change="handleVideoUpload"
|
||
style="width: 0; height: 0; cursor: pointer; opacity: 0"
|
||
/>
|
||
|
||
<!-- 主富文本编辑器 -->
|
||
<div class="editor">
|
||
<QuillEditor
|
||
id="mainEditor"
|
||
ref="myQuillEditor"
|
||
v-model:content="editorContent"
|
||
contentType="html"
|
||
@update:content="onContentChange"
|
||
:options="options"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 标签页配置弹窗 -->
|
||
<div>
|
||
<el-dialog
|
||
v-model="outerVisible"
|
||
title="标签页配置"
|
||
style="width: 1200px; height: 900px"
|
||
close-on-click-modal
|
||
close-on-press-escape
|
||
:before-close="handleBeforeClose"
|
||
>
|
||
<el-tabs
|
||
v-model="activeName"
|
||
type="card"
|
||
class="demo-tabs"
|
||
editable
|
||
@edit="handleTabsEdit"
|
||
@tab-change="handleTabChange"
|
||
>
|
||
<!-- 标签页:标题支持编辑 -->
|
||
<el-tab-pane
|
||
:label="item.title"
|
||
:name="item.key"
|
||
v-for="(item, index) in tabsData"
|
||
:key="item.key"
|
||
@keydown.delete.stop
|
||
@keydown.backspace.stop
|
||
>
|
||
<template #label>
|
||
<div class="tab-title-edit">
|
||
<!-- 文字显示状态 -->
|
||
<span v-if="!item.isEditing" @click="startEditTitle(index)" class="title-text">
|
||
{{ item.title }}
|
||
</span>
|
||
<!-- 输入框编辑状态 -->
|
||
<el-input
|
||
@click.stop
|
||
@keydown.delete.stop
|
||
@keydown.backspace.stop
|
||
v-else
|
||
v-model="item.title"
|
||
max-length=""
|
||
:ref="el => (editInputRefs[index] = el)"
|
||
size="small"
|
||
class="title-input"
|
||
@blur="finishEditTitle(index)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 标签页编辑器内容 -->
|
||
<QuillEditor
|
||
:id="`tabEditor_${item.key}`"
|
||
:ref="
|
||
el => {
|
||
if (el) tabEditors[index] = el;
|
||
}
|
||
"
|
||
v-model:content="item.content"
|
||
contentType="html"
|
||
:options="options1"
|
||
/>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="handleQX">取消</el-button>
|
||
<el-button type="primary" @click="handleQR"> 确认 </el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="Editor">
|
||
import { QuillEditor, Quill } from "@vueup/vue-quill";
|
||
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||
import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted, nextTick } from "vue";
|
||
import { generateUUID } from "@/utils";
|
||
// import { h } from "@/utils/url";
|
||
import { routerObj } from "./utils.js";
|
||
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",
|
||
"34px",
|
||
"36px",
|
||
"38px",
|
||
"40px",
|
||
"42px"
|
||
];
|
||
Quill.register(fontSizeStyle, true);
|
||
|
||
// 自定义Blot
|
||
import ImageBlot from "./quill-image";
|
||
import Video from "./quill-video";
|
||
import TabsBlot from "./quill-tabs";
|
||
import DynamicDivBlot from "./quill-detail-div";
|
||
Quill.register(Video);
|
||
Quill.register(ImageBlot);
|
||
Quill.register(TabsBlot);
|
||
Quill.register(DynamicDivBlot);
|
||
// 基础变量
|
||
const { proxy } = getCurrentInstance();
|
||
const emit = defineEmits(["update:content", "handleRichTextContentChange"]);
|
||
const uuid = ref("id-" + generateUUID());
|
||
const $router = useRouter();
|
||
const routerValueName = $router.currentRoute.value.name;
|
||
const routerName = ref(routerObj[routerValueName]);
|
||
const uploadFileVideo = ref(null);
|
||
const outerVisible = ref(false);
|
||
const imageList = ref([]);
|
||
const imageListDb = ref([]);
|
||
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: "" },
|
||
readOnly: { type: Boolean, default: false },
|
||
fileSizeLimit: { type: Number, default: 10 }
|
||
});
|
||
|
||
// 主编辑器内容双向绑定
|
||
const editorContent = computed({
|
||
get: () => {
|
||
if (!props.content) return "";
|
||
return props.content;
|
||
},
|
||
set: val => {
|
||
emit("update:content", val);
|
||
}
|
||
});
|
||
const myQuillEditor = ref(null); // 主编辑器ref
|
||
|
||
// 主编辑器配置(保持不变)
|
||
const options = reactive({
|
||
theme: "snow",
|
||
debug: "warn",
|
||
strict: false,
|
||
modules: {
|
||
toolbar: {
|
||
container: [
|
||
["bold", "italic", "underline", "strike"],
|
||
["blockquote", "code-block"],
|
||
[{ list: "ordered" }, { list: "bullet" }],
|
||
[{ indent: "-1" }, { indent: "+1" }],
|
||
[{ size: fontSizeStyle.whitelist }],
|
||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||
[{ color: [] }, { background: [] }],
|
||
[{ align: [] }],
|
||
["clean"],
|
||
["link", "image", "video", "tabs"]
|
||
],
|
||
handlers: {
|
||
image: function (value) {
|
||
if (value) {
|
||
activeEditor.value = "main";
|
||
proxy.$refs.uploadRef.click();
|
||
} else Quill.format("customImage", true);
|
||
},
|
||
video: function (value) {
|
||
if (value) {
|
||
activeEditor.value = "main";
|
||
document.querySelector("#uploadFileVideo")?.click();
|
||
} else Quill.format("customVideo", true);
|
||
},
|
||
tabs: function (value) {
|
||
outerVisible.value = value;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
placeholder: "请输入内容...",
|
||
readOnly: props.readOnly
|
||
});
|
||
|
||
// 标签页编辑器配置(保持不变)
|
||
const options1 = reactive({
|
||
theme: "snow",
|
||
debug: "warn",
|
||
modules: {
|
||
toolbar: {
|
||
container: [
|
||
["bold", "italic", "underline", "strike"],
|
||
["blockquote", "code-block"],
|
||
[{ list: "ordered" }, { list: "bullet" }],
|
||
[{ indent: "-1" }, { indent: "+1" }],
|
||
[{ size: fontSizeStyle.whitelist }],
|
||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||
[{ color: [] }, { background: [] }],
|
||
[{ align: [] }],
|
||
["clean"],
|
||
["link", "image", "video"]
|
||
],
|
||
handlers: {
|
||
image: function (value) {
|
||
if (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.key === activeName.value);
|
||
activeEditor.value = `tab-${currentIndex}`;
|
||
document.querySelector("#uploadFileVideo")?.click();
|
||
} else Quill.format("customVideo", true);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
placeholder: "请输入内容...",
|
||
readOnly: props.readOnly
|
||
});
|
||
|
||
// 上传前校验(保持不变)
|
||
const handleBeforeUpload = file => {
|
||
const fileType = file.type;
|
||
file.customUid = generateUUID();
|
||
imageListDb.value.push(file);
|
||
|
||
const validTypes = ["image/jpeg", "image/png", "image/gif", "image/jpg", "image/bmp", "image/webp"];
|
||
if (!validTypes.includes(fileType)) {
|
||
ElNotification({ title: "格式错误", message: "仅支持图片格式", type: "warning" });
|
||
imageListDb.value = imageListDb.value.filter(item => item.customUid !== file.customUid);
|
||
return false;
|
||
}
|
||
|
||
const isLt = file.size / 1024 / 1024 < props.fileSizeLimit;
|
||
if (!isLt) {
|
||
ElNotification({ title: "大小超限", message: `不能超过 ${props.fileSizeLimit} MB`, type: "warning" });
|
||
imageListDb.value = imageListDb.value.filter(item => item.customUid !== file.customUid);
|
||
return false;
|
||
}
|
||
console.log(imageListDb.value, "=================value==================");
|
||
return true;
|
||
};
|
||
|
||
// // 图片上传(保持不变)
|
||
// const handleHttpUpload = async options => {
|
||
// let formData = new FormData();
|
||
// formData.append("image", options.file);
|
||
// imageList.value.push(options.file);
|
||
|
||
// try {
|
||
// const result = await uploadImg(formData, routerName.value, options.file.customUid);
|
||
// if (result?.data?.code === 0) {
|
||
// 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 = "";
|
||
// let quill = "";
|
||
// if (activeEditor.value === "main") {
|
||
// rawQuillEditor = toRaw(myQuillEditor.value);
|
||
// quill = rawQuillEditor.getQuill();
|
||
// } else {
|
||
// const tabIndex = parseInt(activeEditor.value.split("-")[1]);
|
||
// rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
|
||
// quill = rawQuillEditor.getQuill();
|
||
// }
|
||
|
||
// imageListDb.value.forEach(item => {
|
||
// const length = quill.getLength() - 1;
|
||
// quill.insertEmbed(length, "customImage", {
|
||
// url: item.path,
|
||
// id: item.serverImgId || generateUUID()
|
||
// });
|
||
// quill.setSelection(length + 1);
|
||
// });
|
||
|
||
// const finalLength = quill.getLength();
|
||
// quill.setSelection(finalLength);
|
||
|
||
// imageList.value = [];
|
||
// imageListDb.value = [];
|
||
// }
|
||
// }
|
||
// } 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 handleHttpUpload = async options => {
|
||
let formData = new FormData();
|
||
formData.append("image", options.file);
|
||
// imageList.value.push(options.file);
|
||
|
||
try {
|
||
const result = await uploadImg(formData, routerName.value, options.file.customUid);
|
||
if (result?.data?.code === 0) {
|
||
const { data } = result.data;
|
||
const { imgId } = result;
|
||
// imageListDb.value.forEach(item => {
|
||
// if (item.customUid === imgId) {
|
||
// item.serverImgId = imgId;
|
||
// item.path = data.path;
|
||
// console.log(item.path, "================>");
|
||
// }
|
||
// });
|
||
const fileItem = imageListDb.value.find(item => item.customUid === imgId);
|
||
console.log(fileItem, "=fileItem=");
|
||
if (fileItem) {
|
||
fileItem.serverImgId = imgId;
|
||
fileItem.path = data.path;
|
||
}
|
||
const allFilesUploaded = imageListDb.value.every(item => item.path);
|
||
if (allFilesUploaded) {
|
||
let rawQuillEditor = "";
|
||
let quill = "";
|
||
if (activeEditor.value === "main") {
|
||
rawQuillEditor = toRaw(myQuillEditor.value);
|
||
quill = rawQuillEditor.getQuill();
|
||
} else {
|
||
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
|
||
rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
|
||
quill = rawQuillEditor.getQuill();
|
||
}
|
||
|
||
// 关键修改:获取当前光标位置(选区起始索引)
|
||
const selection = quill.getSelection();
|
||
// 如果没有选区(光标未激活),默认插入到末尾
|
||
const insertPosition = selection ? selection.index : quill.getLength();
|
||
imageListDb?.value?.forEach(item => {
|
||
// 使用光标位置插入图片
|
||
quill.insertEmbed(insertPosition, "customImage", {
|
||
url: item.path,
|
||
id: item.serverImgId || generateUUID()
|
||
});
|
||
// 插入后光标后移一位(避免多张图片重叠插入)
|
||
quill.setSelection(insertPosition + 1);
|
||
});
|
||
|
||
// 最终光标定位到最后一张图片后面
|
||
const finalPosition = insertPosition + imageListDb.value.length;
|
||
quill.setSelection(finalPosition);
|
||
|
||
imageList.value = [];
|
||
imageListDb.value = [];
|
||
}
|
||
}
|
||
} 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();
|
||
// formData.append("video", evt.target.files[0]);
|
||
// try {
|
||
// let rawQuillEditor = "";
|
||
// let quill = "";
|
||
// if (activeEditor.value === "main") {
|
||
// rawQuillEditor = toRaw(myQuillEditor.value);
|
||
// quill = rawQuillEditor.getQuill();
|
||
// } else {
|
||
// const tabIndex = parseInt(activeEditor.value.split("-")[1]);
|
||
// rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
|
||
// quill = rawQuillEditor.getQuill();
|
||
// }
|
||
// let length = quill.selection.savedRange.index;
|
||
// const { data } = await uploadVideo(formData);
|
||
// quill.insertEmbed(length, "customVideo", {
|
||
// url: data.path,
|
||
// id: generateUUID()
|
||
// });
|
||
// uploadFileVideo.value.value = "";
|
||
// } catch (error) {
|
||
// console.log(error);
|
||
// }
|
||
// };
|
||
// 在<script setup>中替换handleVideoUpload方法
|
||
const handleVideoUpload = async evt => {
|
||
if (evt.target.files.length === 0) return;
|
||
const file = evt.target.files[0];
|
||
|
||
// 1. 校验视频文件
|
||
const maxSize = props.fileSizeLimit * 1024 * 1024 * 15;
|
||
if (file.size > maxSize) {
|
||
ElNotification({
|
||
title: "文件过大",
|
||
message: `视频大小不能超过 ${props.fileSizeLimit}MB`,
|
||
type: "warning"
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 2. 生成视频本地URL(用于生成封面,不上传)
|
||
const localVideoUrl = URL.createObjectURL(file);
|
||
|
||
try {
|
||
// 4. 并行处理:上传视频 + 生成并上传封面
|
||
// 4.1 上传视频到视频服务器
|
||
const videoFormData = new FormData();
|
||
videoFormData.append("video", file);
|
||
const videoRes = await uploadVideo(videoFormData); // 视频上传接口
|
||
|
||
// 校验视频上传结果(根据你的接口返回格式调整)
|
||
if (videoRes?.code !== 0) {
|
||
throw new Error(`视频上传失败: ${videoRes?.message || "未知错误"}`);
|
||
}
|
||
const videoUrl = videoRes.data.path; // 服务器返回的视频URL
|
||
console.log(localVideoUrl, "=localVideoUrl=");
|
||
// 4.2 生成封面图并上传到图片服务器
|
||
const frameBlob = await Video.captureVideoFrame(localVideoUrl);
|
||
console.log(frameBlob, "============frameBlob===========");
|
||
let coverUrl = "";
|
||
if (!frameBlob) return;
|
||
|
||
// 复用图片上传接口(与图片上传逻辑一致)
|
||
const coverFormData = new FormData();
|
||
const coverUid = generateUUID(); // 生成唯一ID
|
||
// formData.append("image", options.file);
|
||
coverFormData.append("image", frameBlob, `cover-${coverUid}.jpg`);
|
||
console.log(coverFormData, "=coverFormData=");
|
||
// 调用图片上传接口(和普通图片上传用同一个接口)
|
||
const coverRes = await uploadImg(coverFormData, routerName.value, coverUid);
|
||
|
||
// 校验封面上传结果
|
||
if (coverRes?.data?.code === 0) {
|
||
coverUrl = coverRes.data.data.path; // 服务器返回的封面URL
|
||
} else {
|
||
console.warn("封面上传失败,使用默认封面");
|
||
}
|
||
|
||
// 5. 将带封面的视频插入编辑器
|
||
insertVideoToEditor(videoUrl, coverUrl);
|
||
|
||
// 6. 上传成功提示
|
||
// loading.close();
|
||
// ElNotification({
|
||
// title: "上传成功",
|
||
// message: "视频已添加到编辑器",
|
||
// type: "success"
|
||
// });
|
||
} catch (error) {
|
||
console.log(error, "==============");
|
||
} finally {
|
||
console.log("======12323232========");
|
||
// 清理资源
|
||
URL.revokeObjectURL(localVideoUrl); // 释放本地视频URL
|
||
evt.target.value = ""; // 重置文件输入框
|
||
}
|
||
};
|
||
|
||
// 辅助方法:插入视频到编辑器
|
||
const insertVideoToEditor = (videoUrl, coverUrl) => {
|
||
// 获取当前活跃的编辑器(主编辑器或标签页编辑器)
|
||
let quill;
|
||
if (activeEditor.value === "main") {
|
||
quill = toRaw(myQuillEditor.value)?.getQuill();
|
||
} else {
|
||
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
|
||
quill = toRaw(tabEditors.value[tabIndex])?.getQuill();
|
||
}
|
||
|
||
if (quill) {
|
||
const range = quill.getSelection() || { index: 0 };
|
||
// 插入自定义视频组件,携带服务器返回的视频URL和封面URL
|
||
quill.insertEmbed(range.index, "customVideo", {
|
||
url: videoUrl,
|
||
poster: coverUrl // 这里使用图片服务器返回的封面URL
|
||
});
|
||
quill.setSelection(range.index + 1); // 移动光标到视频后
|
||
}
|
||
};
|
||
// 标签页切换事件(基于key切换)
|
||
const handleTabChange = key => {
|
||
const tabIndex = tabsData.value.findIndex(item => item.key === key);
|
||
activeName.value = key;
|
||
activeEditor.value = `tab-${tabIndex}`;
|
||
};
|
||
|
||
// 标签页增删事件
|
||
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({
|
||
key: newKey,
|
||
title: `标签${newIndex + 1}`,
|
||
content: "",
|
||
isEditing: false // 新增时默认不编辑
|
||
});
|
||
nextTick(() => {
|
||
activeName.value = newKey;
|
||
activeEditor.value = `tab-${newIndex}`;
|
||
// 新增后自动进入编辑状态
|
||
setTimeout(() => {
|
||
startEditTitle(newIndex);
|
||
}, 100);
|
||
});
|
||
} else if (action === "remove") {
|
||
// 删除标签页
|
||
const index = tabsData.value.findIndex(item => item.key === targetKey);
|
||
tabsData.value.splice(index, 1);
|
||
tabEditors.value.splice(index, 1);
|
||
editInputRefs.value.splice(index, 1);
|
||
|
||
// 调整活跃编辑器索引
|
||
if (activeEditor.value.startsWith("tab-")) {
|
||
const currentTabIndex = parseInt(activeEditor.value.split("-")[1]);
|
||
if (currentTabIndex > index) {
|
||
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) 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.insertText(range.index, "\n"); // 插入换行
|
||
quill.setSelection(range.index + 1);
|
||
quill.insertText(range.index, "\n"); // 插入换行
|
||
}
|
||
// 关闭弹窗并清空临时数据
|
||
setTabsInfo();
|
||
};
|
||
//取消
|
||
const handleQX = () => {
|
||
setTabsInfo();
|
||
};
|
||
const initTitle = () => {
|
||
const editor = document.querySelector(".ql-editor");
|
||
if (editor) editor.dataset.placeholder = "";
|
||
titleConfig.value.forEach(item => {
|
||
const tip = document.querySelector(`.ql-toolbar ${item.Choice}`);
|
||
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";
|
||
}
|
||
});
|
||
};
|
||
|
||
// const cleanEmptyTags = container => {
|
||
// if (!container) return;
|
||
|
||
// // 获取所有 <p> 标签
|
||
// const pTags = container.querySelectorAll("p");
|
||
|
||
// // 只清理真正的空标签(不含任何内容,包括<br>)
|
||
// Array.from(pTags).forEach(pTag => {
|
||
// // 判断标准:
|
||
// // 1. 完全没有子节点
|
||
// // 2. 或innerHTML为空字符串
|
||
// const isCompletelyEmpty = pTag.childNodes.length === 0 || pTag.innerHTML.trim() === "";
|
||
|
||
// if (isCompletelyEmpty) {
|
||
// // 特殊处理:保留首尾空标签,避免编辑器异常
|
||
// const isFirstOrLast = pTag === container.firstElementChild || pTag === container.lastElementChild;
|
||
|
||
// if (isFirstOrLast) {
|
||
// pTag.innerHTML = ""; // 清空内容
|
||
// } else {
|
||
// pTag.remove(); // 移除中间的纯空标签
|
||
// }
|
||
// }
|
||
// });
|
||
// };
|
||
// // 触发清理的函数(针对所有编辑器)
|
||
// const handleCleanEmptyTags = () => {
|
||
// // 处理主编辑器
|
||
// const mainEditor = document.querySelector("#mainEditor .ql-editor");
|
||
// console.log(mainEditor, "=mainEditor=");
|
||
// if (mainEditor) cleanEmptyTags(mainEditor);
|
||
// };
|
||
|
||
onMounted(() => {
|
||
initTitle();
|
||
// 监听编辑按钮点击事件
|
||
const editorEl = document.querySelector(".ql-editor");
|
||
if (editorEl) {
|
||
editorEl.addEventListener("edit-tabs", e => {
|
||
const tabsData = TabsBlot.value(e.detail.blot.domNode);
|
||
if (tabsData.length > 0) {
|
||
// 保存当前编辑的标签页引用
|
||
currentEditingTabsRef.value = e.detail.blot;
|
||
// 加载数据到弹窗
|
||
loadTabsDataToEditor(tabsData);
|
||
// 显示弹窗
|
||
outerVisible.value = true;
|
||
}
|
||
});
|
||
}
|
||
// 等待编辑器首次渲染完成
|
||
// setTimeout(handleCleanEmptyTags, 300);
|
||
});
|
||
// 监听内容变化,重新清理空标签
|
||
// watch(editorContent, () => {
|
||
// nextTick(() => {
|
||
// setTimeout(handleCleanEmptyTags, 100); // 延迟确保 Quill 已重新渲染
|
||
// });
|
||
// });
|
||
defineExpose({
|
||
clearEditor: () => {
|
||
const quill = toRaw(myQuillEditor.value)?.getQuill();
|
||
if (quill) {
|
||
quill.setText("");
|
||
editorContent.value = "";
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
@import "./index.scss";
|
||
|
||
// 增加编辑器内容区交互性确保删除可用
|
||
.ql-editor {
|
||
min-height: 600px; // 确保空编辑器也有点击区域
|
||
cursor: text !important;
|
||
user-select: text !important;
|
||
}
|
||
|
||
// /* 标签页样式 */
|
||
.quill-tabs {
|
||
margin: 15px 0;
|
||
overflow: hidden;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
// /* 用伪元素添加图标(可替换为自己的图标) */
|
||
.ql-tabs::before {
|
||
font-size: 16px;
|
||
content: "T"; /* 用 emoji 或字体图标 */
|
||
}
|
||
.title-input {
|
||
width: 100px;
|
||
margin: -2px 0; /* 与标签对齐 */
|
||
}
|
||
|
||
/* 详情样式 */
|
||
.o_detail_all {
|
||
overflow: hidden;
|
||
text-align: center;
|
||
background-color: #ffffff;
|
||
}
|
||
.o_detail_title {
|
||
margin-top: 2vw;
|
||
margin-bottom: 1.25vw;
|
||
overflow: hidden;
|
||
font-size: 2.25em;
|
||
font-weight: 600;
|
||
line-height: 1.2em;
|
||
color: #101010;
|
||
text-align: center;
|
||
background-color: #ffffff;
|
||
}
|
||
.o_detail_small {
|
||
margin-bottom: 0.7vw;
|
||
font-size: 1.5em;
|
||
color: #333333;
|
||
}
|
||
.o_detail_text {
|
||
width: 80%;
|
||
margin-right: auto;
|
||
margin-bottom: 0.7vw;
|
||
margin-left: auto;
|
||
font-size: 1.125em;
|
||
line-height: 1.5em;
|
||
color: #737373;
|
||
}
|
||
.products_des {
|
||
width: 100%;
|
||
margin-bottom: 50px;
|
||
}
|
||
.products_des img {
|
||
width: 100%;
|
||
}
|
||
.de_t_n {
|
||
font-size: 1.5em;
|
||
color: #333333;
|
||
}
|
||
.detail_title {
|
||
padding: 2% 0;
|
||
text-align: center;
|
||
}
|
||
.detail_title p {
|
||
line-height: 2em;
|
||
}
|
||
.detail_con_a {
|
||
margin: auto;
|
||
overflow: hidden;
|
||
}
|
||
.lj_detail_text,
|
||
.lj_detail_texts {
|
||
font-size: 0.875em;
|
||
}
|
||
.lj_detail_text p {
|
||
padding: 0.5% 0;
|
||
line-height: 1.6em;
|
||
}
|
||
|
||
/* seo-pro */
|
||
.seo-pro h3 {
|
||
margin: 2% 0 1%;
|
||
font-size: 1.5em;
|
||
font-weight: 400;
|
||
line-height: 1.2;
|
||
color: #333333;
|
||
text-align: center;
|
||
}
|
||
.seo-pro p {
|
||
margin: 0 0 11px;
|
||
text-align: center;
|
||
}
|
||
.seo-pro a {
|
||
color: #333333;
|
||
text-decoration: none;
|
||
}
|
||
.sa_blue,
|
||
.sa_blue a,
|
||
.seo-pro a:hover {
|
||
color: #009fdf;
|
||
}
|
||
</style>
|