feat: 🚀 图片排序

This commit is contained in:
2025-09-17 13:54:30 +08:00
parent 0dab4cc524
commit 1090351df7
2 changed files with 482 additions and 347 deletions

View File

@@ -54,7 +54,6 @@
@edit="handleTabsEdit"
@tab-change="handleTabChange"
>
<!-- 标签页标题支持编辑 -->
<el-tab-pane
:label="item.title"
:name="item.key"
@@ -65,11 +64,9 @@
>
<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
@@ -85,7 +82,6 @@
</div>
</template>
<!-- 标签页编辑器内容 -->
<QuillEditor
:id="`tabEditor_${item.key}`"
:ref="
@@ -107,20 +103,66 @@
</template>
</el-dialog>
</div>
<!-- 图片顺序调整弹窗 -->
<el-dialog v-model="showImageSortDialog" title="调整图片顺序" width="800px" :before-close="handleImageDialogClose">
<div class="image-sort-container">
<draggable
v-model="sortedImageList"
:animation="200"
class="image-grid"
ghost-class="ghost"
item-key="customUid"
@update:modelValue="updateSortOrder"
>
<template #item="{ element: img }">
<div class="image-item">
<div class="image-preview-container">
<div class="image-order-badge">{{ img.sortOrder + 1 }}</div>
<img :src="img.tempUrl" :alt="`图片 ${img.sortOrder + 1}`" class="preview-img" />
<el-button
:icon="Delete"
size="small"
type="default"
class="circle-delete-btn"
@click.stop="removeImage(img.customUid)"
></el-button>
</div>
<div class="image-info">
<span class="image-name">{{ img.name }}</span>
</div>
</div>
</template>
</draggable>
<div v-if="sortedImageList.length === 0" class="empty-state">暂无图片请先上传</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showImageSortDialog = false">取消</el-button>
<el-button type="primary" @click="confirmInsertImages" :disabled="sortedImageList.length === 0">
确认插入
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup name="Editor">
import { Delete } from "@element-plus/icons-vue";
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";
import draggable from "vuedraggable";
// 字体配置
let fontSizeStyle = Quill.import("attributors/style/size");
fontSizeStyle.whitelist = [
@@ -152,6 +194,7 @@ Quill.register(Video);
Quill.register(ImageBlot);
Quill.register(TabsBlot);
Quill.register(DynamicDivBlot);
// 基础变量
const { proxy } = getCurrentInstance();
const emit = defineEmits(["update:content", "handleRichTextContentChange"]);
@@ -163,16 +206,20 @@ 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-索引
const activeName = ref(null);
const activeEditor = ref("main");
// 标签页数据新增key作为唯一标识isEditing控制编辑状态
// 标签页数据
const tabsData = ref([]);
// 标签页编辑器ref数组
const tabEditors = ref([]);
// 标题编辑输入框的ref
const editInputRefs = ref([]);
const currentEditingTabsRef = ref(null);
// 图片排序相关变量
const showImageSortDialog = ref(false);
const sortedImageList = ref([]);
const uploadingCount = ref(0);
// Props
const props = defineProps({
content: { type: String, default: "" },
@@ -190,9 +237,9 @@ const editorContent = computed({
emit("update:content", val);
}
});
const myQuillEditor = ref(null); // 主编辑器ref
const myQuillEditor = ref(null);
// 主编辑器配置(保持不变)
// 主编辑器配置
const options = reactive({
theme: "snow",
debug: "warn",
@@ -215,6 +262,7 @@ const options = reactive({
image: function (value) {
if (value) {
activeEditor.value = "main";
sortedImageList.value = [];
proxy.$refs.uploadRef.click();
} else Quill.format("customImage", true);
},
@@ -230,12 +278,11 @@ const options = reactive({
}
}
},
placeholder: "请输入内容...",
readOnly: props.readOnly
});
// 标签页编辑器配置(保持不变)
// 标签页编辑器配置
const options1 = reactive({
theme: "snow",
debug: "warn",
@@ -259,6 +306,7 @@ const options1 = reactive({
if (value) {
const currentIndex = tabsData.value.findIndex(item => item.key === activeName.value);
activeEditor.value = `tab-${currentIndex}`;
sortedImageList.value = [];
proxy.$refs.uploadRef.click();
} else Quill.format("customImage", true);
},
@@ -276,186 +324,172 @@ const options1 = reactive({
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==================");
// 生成临时URL用于预览
const tempUrl = URL.createObjectURL(file);
imageListDb.value.push({
...file,
tempUrl,
sortOrder: imageListDb.value.length, // 初始化排序索引
serverImgId: "",
path: ""
});
uploadingCount.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=");
const fileItem = imageListDb.value.find(item => item.customUid === options.file.customUid);
if (fileItem) {
fileItem.serverImgId = imgId;
fileItem.path = data.path;
}
const allFilesUploaded = imageListDb.value.every(item => item.path);
console.log(allFilesUploaded, "=allFilesUploaded=");
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 => {
// 使用光标位置插入图片
setTimeout(() => {
quill.insertEmbed(insertPosition, "customImage", {
url: item.path,
id: item.serverImgId || generateUUID()
uploadingCount.value--;
// 所有图片上传完成后显示排序弹窗
if (uploadingCount.value === 0) {
sortedImageList.value = [...imageListDb.value];
updateSortOrder(); // 确保排序索引正确
nextTick(() => {
showImageSortDialog.value = true;
});
}, 100);
// 插入后光标后移一位(避免多张图片重叠插入)
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 failUid = options.file.customUid;
imageListDb.value = imageListDb.value.filter(item => item.customUid !== failUid);
uploadingCount.value = Math.max(0, uploadingCount.value - 1);
ElNotification({
title: "上传失败",
message: `图片 ${options.file.name} 上传失败`,
type: "error"
});
}
};
// 视频上传(保持不变)
// 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 updateSortOrder = () => {
sortedImageList.value.forEach((item, index) => {
item.sortOrder = index;
});
};
// 从排序列表移除图片
const removeImage = customUid => {
// 释放临时URL
const removedImg = sortedImageList.value.find(img => img.customUid === customUid);
if (removedImg?.tempUrl) {
URL.revokeObjectURL(removedImg.tempUrl);
}
// 从列表中移除
sortedImageList.value = sortedImageList.value.filter(img => img.customUid !== customUid);
// 重新计算排序索引
updateSortOrder();
};
// 确认插入图片到编辑器
const confirmInsertImages = () => {
let quill;
if (activeEditor.value === "main") {
const rawQuillEditor = toRaw(myQuillEditor.value);
quill = rawQuillEditor.getQuill();
} else {
const tabIndex = parseInt(activeEditor.value.split("-")[1]);
const rawQuillEditor = toRaw(tabEditors.value[tabIndex]);
quill = rawQuillEditor.getQuill();
}
if (!quill) {
ElNotification({ title: "错误", message: "编辑器未加载完成", type: "error" });
return;
}
// 获取光标位置
const range = quill.getSelection() || { index: 0 };
let currentInsertIndex = range.index;
// 按排序索引升序插入
const sortedByOrder = [...sortedImageList.value].sort((a, b) => a.sortOrder - b.sortOrder);
sortedByOrder.forEach(item => {
quill.insertEmbed(currentInsertIndex, "customImage", {
url: "https://dev.ow.f2b211.com" + item.path,
id: item.serverImgId || generateUUID()
});
currentInsertIndex++;
});
// 调整光标位置
quill.setSelection(currentInsertIndex);
// 清理资源
sortedImageList.value.forEach(img => {
if (img.tempUrl) {
URL.revokeObjectURL(img.tempUrl);
}
});
// 关闭弹窗并重置
showImageSortDialog.value = false;
imageList.value = [];
imageListDb.value = [];
sortedImageList.value = [];
};
// 关闭图片排序弹窗时清理资源
const handleImageDialogClose = () => {
// 释放所有临时URL
sortedImageList.value.forEach(img => {
if (img.tempUrl) {
URL.revokeObjectURL(img.tempUrl);
}
});
// 重置状态
showImageSortDialog.value = false;
imageList.value = [];
imageListDb.value = [];
sortedImageList.value = [];
};
// 视频上传
const handleVideoUpload = async evt => {
if (evt.target.files.length === 0) return;
const file = evt.target.files[0];
// 1. 校验视频文件
// 校验视频文件
const maxSize = 150 * 1024 * 1024;
if (file.size > maxSize) {
ElNotification({
@@ -467,67 +501,46 @@ const handleVideoUpload = async evt => {
return;
}
// 2. 生成视频本地URL(用于生成封面,不上传)
// 生成视频本地URL
const localVideoUrl = URL.createObjectURL(file);
try {
// 4. 并行处理:上传视频 + 生成并上传封面
// 4.1 上传视频到视频服务器
// 上传视频
const videoFormData = new FormData();
videoFormData.append("video", file);
const videoRes = await uploadVideo(videoFormData); // 视频上传接口
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 videoUrl = videoRes.data.path;
// 复用图片上传接口(与图片上传逻辑一致)
// 生成封面图并上传
const frameBlob = await Video.captureVideoFrame(localVideoUrl);
let coverUrl = "";
if (frameBlob) {
const coverFormData = new FormData();
const coverUid = generateUUID(); // 生成唯一ID
// formData.append("image", options.file);
const coverUid = generateUUID();
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("封面上传失败,使用默认封面");
coverUrl = coverRes.data.data.path;
}
}
// 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 = ""; // 重置文件输入框
URL.revokeObjectURL(localVideoUrl);
evt.target.value = "";
}
};
// 辅助方法:插入视频到编辑器
// 插入视频到编辑器
const insertVideoToEditor = (videoUrl, coverUrl) => {
// 获取当前活跃的编辑器(主编辑器或标签页编辑器)
let quill;
if (activeEditor.value === "main") {
quill = toRaw(myQuillEditor.value)?.getQuill();
@@ -538,15 +551,15 @@ const insertVideoToEditor = (videoUrl, coverUrl) => {
if (quill) {
const range = quill.getSelection() || { index: 0 };
// 插入自定义视频组件携带服务器返回的视频URL和封面URL
quill.insertEmbed(range.index, "customVideo", {
url: videoUrl,
poster: coverUrl // 这里使用图片服务器返回的封面URL
poster: coverUrl
});
quill.setSelection(range.index + 1); // 移动光标到视频后
quill.setSelection(range.index + 1);
}
};
// 标签页切换事件基于key切换
// 标签页切换事件
const handleTabChange = key => {
const tabIndex = tabsData.value.findIndex(item => item.key === key);
activeName.value = key;
@@ -559,37 +572,32 @@ const handleTabsEdit = (targetKey, action) => {
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 // 新增时默认不编辑
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;
}
@@ -601,48 +609,41 @@ const handleTabsEdit = (targetKey, action) => {
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; // 触发重绘
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;
@@ -651,26 +652,16 @@ const handleQR = () => {
}
const range = quill.getSelection(true);
// 判断是否是编辑已有标签页(通过 currentEditingTabsRef 是否有值)
if (currentEditingTabsRef.value) {
// 1. 编辑模式:更新原有标签页组件
const blot = currentEditingTabsRef.value;
// 更新 blot 的数据(触发 DOM 更新)
blot.updateContents(tabsData.value); // 需要在 TabsBlot 中添加 updateContents 方法
// 清除编辑状态标记
currentEditingTabsRef.value.updateContents(tabsData.value);
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"); // 插入换行
quill.insertText(range.index, "\n");
}
// 关闭弹窗并清空临时数据
setTabsInfo();
};
//取消
const handleQX = () => {
setTabsInfo();
};
@@ -682,20 +673,16 @@ 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 // 编辑状态标记
key: `tab_${generateUUID()}`,
title: tab.title || `标签${index + 1}`,
content: tab.content || "",
isEditing: false
});
});
// 激活第一个标签页(如果有数据)
nextTick(() => {
if (tabsData.value.length > 0) {
activeName.value = tabsData.value[0].key;
@@ -704,65 +691,21 @@ const loadTabsDataToEditor = tabs => {
});
};
// 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();
@@ -777,31 +720,145 @@ defineExpose({
<style lang="scss">
@import "./index.scss";
// 增加编辑器内容区交互性确保删除可用
// 编辑器基础样式
.ql-editor {
min-height: 600px; // 确保空编辑器也有点击区域
min-height: 600px;
cursor: text !important;
user-select: text !important;
}
// /* 标签页样式 */
// 图片排序弹窗样式
.image-sort-container {
margin-top: 15px;
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
padding: 10px;
}
.image-item {
width: 150px;
padding: 8px;
cursor: move;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
}
}
// 图片预览容器
.image-preview-container {
position: relative;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
}
.preview-img {
display: block;
width: 100%;
height: 120px;
object-fit: cover;
}
// 排序序号徽章
.image-order-badge {
position: absolute;
top: 5px;
left: 5px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 12px;
color: white;
background: rgb(0 0 0 / 50%);
border-radius: 50%;
}
// 圆形删除按钮
.circle-delete-btn {
position: absolute;
top: -8px;
right: -8px;
z-index: 500;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
margin: 0;
color: white;
background-color: #ff4d4f;
border: none;
border-radius: 50%;
box-shadow: 0 2px 4px rgb(0 0 0 / 20%);
transition: all 0.2s;
&:hover {
color: white;
background-color: #d93025;
transform: scale(1.1);
}
.el-icon {
font-size: 14px;
}
}
.image-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
font-size: 12px;
}
.image-name {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.empty-state {
padding: 30px 0;
font-size: 14px;
color: #666666;
text-align: center;
}
.ghost {
background-color: #e9ecef;
opacity: 0.5;
}
// 标签页样式
.quill-tabs {
margin: 15px 0;
overflow: hidden;
border-radius: 4px;
}
// /* 用伪元素添加图标(可替换为自己的图标) */
.ql-tabs::before {
font-size: 16px;
content: "T"; /* 用 emoji 或字体图标 */
content: "T";
}
.title-input {
width: 100px;
margin: -2px 0; /* 与标签对齐 */
margin: -2px 0;
}
/* 详情样式 */
// 图片样式
.ql-editor .quill-image {
max-width: 100%;
height: auto;
margin: 5px 0;
&:focus {
outline: 2px solid #4285f4;
outline-offset: 2px;
}
}
// 详情样式
.o_detail_all {
overflow: hidden;
text-align: center;

View File

@@ -177,7 +177,7 @@ const currentEditingTabsRef = ref(null);
const props = defineProps({
content: { type: String, default: "" },
readOnly: { type: Boolean, default: false },
fileSizeLimit: { type: Number, default: 10 }
fileSizeLimit: { type: Number, default: 5 }
});
// 主编辑器内容双向绑定
@@ -239,6 +239,7 @@ const options = reactive({
const options1 = reactive({
theme: "snow",
debug: "warn",
strict: false,
modules: {
toolbar: {
container: [
@@ -279,7 +280,6 @@ const options1 = reactive({
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)) {
@@ -294,10 +294,82 @@ const handleBeforeUpload = file => {
imageListDb.value = imageListDb.value.filter(item => item.customUid !== file.customUid);
return false;
}
imageListDb.value.push({
...file,
order: imageListDb.value.length
});
imageListDb.value = [...imageListDb.value].sort((a, b) => a.order - b.order);
console.log(imageListDb.value, "=================value==================");
return true;
};
const handleHttpUpload = async options => {
let formData = new FormData();
formData.append("image", 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);
console.log(fileItem, "=fileItem=");
if (fileItem) {
fileItem.serverImgId = imgId;
fileItem.path = data.path;
// 记录完成时间,用于排序
fileItem.completeTime = Date.now();
}
const allFilesUploaded = imageListDb.value.every(item => item.path);
console.log(allFilesUploaded, "=allFilesUploaded=");
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中的顺序就是上传顺序
// 或者如果有order属性可以按照order排序
const sortedImages = [...imageListDb.value];
// 按顺序插入图片
sortedImages.forEach((item, index) => {
quill.insertEmbed(insertPosition + index, "customImage", {
url: "https://dev.ow.f2b211.com" + item.path,
id: item.serverImgId || generateUUID()
});
});
// 最终光标定位到最后一张图片后面
const finalPosition = insertPosition + sortedImages.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 handleHttpUpload = async options => {
// let formData = new FormData();
@@ -352,71 +424,76 @@ const handleBeforeUpload = file => {
// }
// };
// // 图片上传(修改插入位置逻辑)
const handleHttpUpload = async options => {
let formData = new FormData();
formData.append("image", options.file);
// imageList.value.push(options.file);
// // // 图片上传(修改插入位置逻辑)
// 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, "================>");
// 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);
// console.log(allFilesUploaded, "=allFilesUploaded=");
// 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();
// console.log(imageListDb, "=imageListDb=");
// imageListDb?.value?.forEach(item => {
// // 使用光标位置插入图片
// setTimeout(() => {
// quill.insertEmbed(insertPosition, "customImage", {
// url: "https://dev.ow.f2b211.com" + item.path,
// id: item.serverImgId || generateUUID()
// });
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();
}
// }, 100);
// 关键修改:获取当前光标位置(选区起始索引
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);
});
// // 插入后光标后移一位(避免多张图片重叠插入
// quill.setSelection(insertPosition + 1);
// });
// 最终光标定位到最后一张图片后面
const finalPosition = insertPosition + imageListDb.value.length;
quill.setSelection(finalPosition);
// // 最终光标定位到最后一张图片后面
// 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);
}
};
// 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 => {
@@ -451,13 +528,14 @@ const handleVideoUpload = async evt => {
const file = evt.target.files[0];
// 1. 校验视频文件
const maxSize = props.fileSizeLimit * 1024 * 1024 * 15;
const maxSize = 150 * 1024 * 1024;
if (file.size > maxSize) {
ElNotification({
title: "文件过大",
message: `视频大小不能超过 ${props.fileSizeLimit}MB`,
message: `视频大小不能超过 ${150}MB`,
type: "warning"
});
evt.target.value = "";
return;
}