feat: 🚀 tabs

This commit is contained in:
2025-07-29 11:41:09 +08:00
parent 1a97f75546
commit 41da6b1914
10 changed files with 451 additions and 153 deletions

View File

@@ -29,8 +29,8 @@
<QuillEditor
id="mainEditor"
ref="myQuillEditor"
v-model:content="editorContent"
contentType="html"
v-model:content="editorContent"
@update:content="onContentChange"
:options="options"
/>
@@ -53,6 +53,7 @@
editable
@edit="handleTabsEdit"
@tab-change="handleTabChange"
v-if="tabsData.length"
>
<!-- 标签页标题支持编辑 -->
<el-tab-pane
@@ -112,7 +113,8 @@
<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";
// /computed
import { getCurrentInstance, reactive, ref, toRaw, onMounted, nextTick } from "vue";
import { generateUUID } from "@/utils";
// import { h } from "@/utils/url";
import { routerObj } from "./utils.js";
@@ -126,7 +128,7 @@ let fontSizeStyle = Quill.import("attributors/style/size");
fontSizeStyle.whitelist = ["12px", "14px", "16px", "18px", "20px", "22px", "24px", "26px", "28px", "30px", "32px"];
Quill.register(fontSizeStyle, true);
// 自定义Blot
// 自定义Blot;
import ImageBlot from "./quill-image";
import Video from "./quill-video";
import TabsBlot from "./quill-tabs";
@@ -151,7 +153,7 @@ const activeEditor = ref("main"); // 跟踪当前活跃编辑器main/tab-索
// 标签页数据新增key作为唯一标识isEditing控制编辑状态
const tabsData = ref([]);
// 标签页编辑器ref数组
const tabEditors = ref([]);
const tabEditors = reactive([]);
// 标题编辑输入框的ref
const editInputRefs = ref([]);
const currentEditingTabsRef = ref(null);
@@ -164,7 +166,9 @@ const props = defineProps({
// 主编辑器内容双向绑定
const editorContent = computed({
get: () => props.content,
get: () => {
return props.content;
},
set: val => {
emit("update:content", val);
}
@@ -175,6 +179,7 @@ const myQuillEditor = ref(null); // 主编辑器ref
const options = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -301,7 +306,7 @@ const handleHttpUpload = async options => {
quill = rawQuillEditor.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
rawQuillEditor = toRaw(tabEditors[tabIndex]);
quill = rawQuillEditor.getQuill();
}
@@ -329,33 +334,127 @@ const handleHttpUpload = async options => {
};
// 视频上传(保持不变)
// 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 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()
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"
});
uploadFileVideo.value.value = "";
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);
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[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);
@@ -390,7 +489,7 @@ const handleTabsEdit = (targetKey, action) => {
// 删除标签页
const index = tabsData.value.findIndex(item => item.key === targetKey);
tabsData.value.splice(index, 1);
tabEditors.value.splice(index, 1);
tabEditors.splice(index, 1);
editInputRefs.value.splice(index, 1);
// 调整活跃编辑器索引
@@ -438,6 +537,7 @@ const finishEditTitle = index => {
// 其他方法(保持不变)
const onContentChange = content => {
console.log(content, "=content=");
emit("handleRichTextContentChange", content);
emit("update:content", content);
};
@@ -515,26 +615,25 @@ const loadTabsDataToEditor = tabs => {
};
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;
}
});
}
nextTick(() => {
initTitle();
// 监听编辑按钮点击事件
const editorEl = document.querySelector(".ql-editor");
if (editorEl) {
editorEl.addEventListener("edit-tabs", e => {
console.log(e.detail.blot, "=e.detail=");
const tabsData = TabsBlot.value(e.detail.blot.domNode);
if (tabsData.length > 0) {
// 保存当前编辑的标签页引用
currentEditingTabsRef.value = e.detail.blot;
// 加载数据到弹窗
loadTabsDataToEditor(tabsData);
// 显示弹窗
outerVisible.value = true;
}
});
}
});
});
defineExpose({
@@ -575,3 +674,4 @@ defineExpose({
margin: -2px 0; /* 与标签对齐 */
}
</style>
./quill-image1111