feat: 🚀 div标签解析

This commit is contained in:
2025-07-30 16:33:56 +08:00
parent 5da9c11771
commit 4e8f3e6564
5 changed files with 284 additions and 27 deletions

BIN
dist.zip

Binary file not shown.

View File

@@ -196,22 +196,23 @@
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before { // .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before {
// content: "32px"; // content: "32px";
// } // }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before, .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before {
// content: "36px"; content: "36px";
// } }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="38px"]::before, .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="38px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="38px"]::before { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="38px"]::before {
// content: "38px"; content: "38px";
// } }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="40px"]::before, .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="40px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="40px"]::before { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="40px"]::before {
// content: "40px"; content: "40px";
// } }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="42px"]::before, .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="42px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="42px"]::before { .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="42px"]::before {
// content: "44px"; content: "44px";
// } }
// .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="44px"]::before, // .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="44px"]::before,
// .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="44px"]::before { // .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="44px"]::before {
// content: "44px"; // content: "44px";

View File

@@ -112,7 +112,7 @@
<script setup name="Editor"> <script setup name="Editor">
import { QuillEditor, Quill } from "@vueup/vue-quill"; import { QuillEditor, Quill } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css"; import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted, nextTick } from "vue"; import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted, nextTick, watch } from "vue";
import { generateUUID } from "@/utils"; import { generateUUID } from "@/utils";
// import { h } from "@/utils/url"; // import { h } from "@/utils/url";
import { routerObj } from "./utils.js"; import { routerObj } from "./utils.js";
@@ -123,17 +123,35 @@ import { useRouter } from "vue-router";
import { useMsg } from "@/hooks/useMsg"; import { useMsg } from "@/hooks/useMsg";
// 字体配置 // 字体配置
let fontSizeStyle = Quill.import("attributors/style/size"); let fontSizeStyle = Quill.import("attributors/style/size");
fontSizeStyle.whitelist = ["12px", "14px", "16px", "18px", "20px", "22px", "24px", "26px", "28px", "30px", "32px"]; fontSizeStyle.whitelist = [
"12px",
"14px",
"16px",
"18px",
"20px",
"22px",
"24px",
"26px",
"28px",
"30px",
"32px",
"34px",
"36px",
"38px",
"40px",
"42px"
];
Quill.register(fontSizeStyle, true); Quill.register(fontSizeStyle, true);
// 自定义Blot // 自定义Blot
import ImageBlot from "./quill-image"; import ImageBlot from "./quill-image";
import Video from "./quill-video"; import Video from "./quill-video";
import TabsBlot from "./quill-tabs"; import TabsBlot from "./quill-tabs";
import DynamicDivBlot from "./quill-detail-div";
Quill.register(Video); Quill.register(Video);
Quill.register(ImageBlot); Quill.register(ImageBlot);
Quill.register(TabsBlot); Quill.register(TabsBlot);
Quill.register(DynamicDivBlot);
// 基础变量 // 基础变量
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const emit = defineEmits(["update:content", "handleRichTextContentChange"]); const emit = defineEmits(["update:content", "handleRichTextContentChange"]);
@@ -164,7 +182,10 @@ const props = defineProps({
// 主编辑器内容双向绑定 // 主编辑器内容双向绑定
const editorContent = computed({ const editorContent = computed({
get: () => props.content, get: () => {
if (!props.content) return "";
return props.content;
},
set: val => { set: val => {
emit("update:content", val); emit("update:content", val);
} }
@@ -175,6 +196,7 @@ const myQuillEditor = ref(null); // 主编辑器ref
const options = reactive({ const options = reactive({
theme: "snow", theme: "snow",
debug: "warn", debug: "warn",
strict: false,
modules: { modules: {
toolbar: { toolbar: {
container: [ container: [
@@ -206,8 +228,17 @@ const options = reactive({
outerVisible.value = value; outerVisible.value = value;
} }
} }
},
clipboard: {
matchVisual: false // 禁用视觉匹配(减少空标签)
} }
}, },
formats: [
// ... 原有格式
"align",
"bold",
"italic" // 只保留必要格式,减少自动生成的空标签
],
placeholder: "请输入内容...", placeholder: "请输入内容...",
readOnly: props.readOnly readOnly: props.readOnly
}); });
@@ -608,20 +639,54 @@ const loadTabsDataToEditor = tabs => {
}); });
}; };
// 清理空标签的专用函数(针对 Quill 生成的空 <p>
const cleanEmptyTags = container => {
if (!container) return;
// 1. 获取所有 <p> 标签
const allPTags = container.querySelectorAll("p");
if (allPTags.length === 0) return;
// 2. 手动判断标签是否为空(处理隐形字符)
Array.from(allPTags).forEach(pTag => {
// 清除所有空白字符(包括 &nbsp;、换行、空格)
const trimmedContent = pTag.innerHTML
.replace(/&nbsp;/g, "") // 替换 HTML 空格实体
.replace(/\s+/g, "") // 替换所有空白字符(空格、换行等)
.replace(/<br\s*\/?>/gi, ""); // 移除 <br> 标签
// 判断是否为空标签(清理后内容长度为 0
const isEmpty = trimmedContent.length === 0;
if (isEmpty) {
// 保留首尾空标签,避免编辑器异常
const isFirstOrLast = pTag === container.firstElementChild || pTag === container.lastElementChild;
if (!isFirstOrLast) {
pTag.remove();
} else {
pTag.innerHTML = ""; // 清空内容
}
}
});
};
// 触发清理的函数(针对所有编辑器)
const handleCleanEmptyTags = () => {
// 处理主编辑器
const mainEditor = document.querySelector("#mainEditor .ql-editor");
console.log(mainEditor, "=mainEditor=");
if (mainEditor) cleanEmptyTags(mainEditor);
};
onMounted(() => { onMounted(() => {
initTitle(); initTitle();
// 监听编辑按钮点击事件 // 监听编辑按钮点击事件
const editorEl = document.querySelector(".ql-editor"); const editorEl = document.querySelector(".ql-editor");
if (editorEl) { if (editorEl) {
editorEl.addEventListener("edit-tabs", e => { editorEl.addEventListener("edit-tabs", e => {
console.log(1232, "测试");
console.log(e.detail.blot, "=e.detail=");
const tabsData = TabsBlot.value(e.detail.blot.domNode); const tabsData = TabsBlot.value(e.detail.blot.domNode);
console.log(tabsData, "=tabsData=");
if (tabsData.length > 0) { if (tabsData.length > 0) {
// 保存当前编辑的标签页引用 // 保存当前编辑的标签页引用
currentEditingTabsRef.value = e.detail.blot; currentEditingTabsRef.value = e.detail.blot;
console.log(currentEditingTabsRef.value, "=currentEditingTabsRef.value =");
// 加载数据到弹窗 // 加载数据到弹窗
loadTabsDataToEditor(tabsData); loadTabsDataToEditor(tabsData);
// 显示弹窗 // 显示弹窗
@@ -629,8 +694,15 @@ onMounted(() => {
} }
}); });
} }
// 等待编辑器首次渲染完成
setTimeout(handleCleanEmptyTags, 300);
});
// 监听内容变化,重新清理空标签
watch(editorContent, () => {
nextTick(() => {
setTimeout(handleCleanEmptyTags, 100); // 延迟确保 Quill 已重新渲染
});
}); });
defineExpose({ defineExpose({
clearEditor: () => { clearEditor: () => {
const quill = toRaw(myQuillEditor.value)?.getQuill(); const quill = toRaw(myQuillEditor.value)?.getQuill();
@@ -668,4 +740,110 @@ defineExpose({
width: 100px; width: 100px;
margin: -2px 0; /* 与标签对齐 */ margin: -2px 0; /* 与标签对齐 */
} }
.o_detail_all {
text-align: center;
}
.o_detail_title {
margin-top: 3.125vw;
margin-bottom: 1.25vw;
font-size: 2.25em;
font-weight: 600;
line-height: 1.2em;
color: #101010;
}
/* stylelint-disable-next-line no-duplicate-selectors */
.o_detail_small {
margin-bottom: 0.7vw;
font-size: 1.5em;
color: #333333;
}
/* stylelint-disable-next-line no-duplicate-selectors */
.o_detail_text {
width: 80%;
margin-right: auto;
margin-bottom: 0.7vw;
margin-left: auto;
font-size: 1em;
line-height: 1.5em;
color: #737373;
}
/* stylelint-disable-next-line no-duplicate-selectors */
.o_detail_title {
padding: 4% 0 2.8%;
font-size: 2.25em;
color: #101010;
}
/* stylelint-disable-next-line no-duplicate-selectors */
.o_detail_small {
padding-bottom: 1.8%;
font-size: 1.5em;
color: #333333;
}
/* stylelint-disable-next-line no-duplicate-selectors */
.o_detail_text {
width: 80%;
padding: 0 0 1.8%;
margin: auto;
font-size: 1.125em;
line-height: 1.875em;
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;
}
/* stylelint-disable-next-line no-duplicate-selectors */
// .o_detail_all {
// text-align: center;
// }
/* stylelint-disable-next-line no-duplicate-selectors */
</style> </style>

View File

@@ -0,0 +1,49 @@
// quill-dynamic-div.js
import { Quill } from "@vueup/vue-quill";
const Block = Quill.import("blots/block");
class DynamicDivBlot extends Block {
// 从 DOM 节点创建 Blot 时,保留所有属性和类名
static create(value) {
const node = super.create();
// 如果是从数据恢复value 存在),还原类名
if (value?.className) {
node.className = value.className; // 直接设置完整类名字符串(支持多类名)
}
// 保留其他属性(如 id、data-* 等,按需添加)
if (value?.attrs) {
Object.keys(value.attrs).forEach(key => {
node.setAttribute(key, value.attrs[key]);
});
}
return node;
}
// 从 Blot 提取值(序列化时用),保留所有类名和属性
static value(node) {
return {
className: node.className, // 保留完整类名(如 "o_detail_all big"
attrs: Array.from(node.attributes).reduce((attrs, attr) => {
attrs[attr.name] = attr.value;
return attrs;
}, {}) // 保留所有属性
};
}
// 格式化时识别所有 div 标签
static formats(domNode) {
// 只要是 div 标签,就返回其类名和属性(确保 Quill 不清理)
return {
className: domNode.className,
attrs: Array.from(domNode.attributes).reduce((attrs, attr) => {
attrs[attr.name] = attr.value;
return attrs;
}, {})
};
}
}
DynamicDivBlot.blotName = "dynamicDiv"; // 唯一标识
DynamicDivBlot.tagName = "div"; // 处理所有 div 标签
export default DynamicDivBlot;

View File

@@ -15,6 +15,35 @@ const hasIdRecursive = (data: any, targetId: any) => {
return false; return false;
}; };
const htmlDecode = (html: any) => {
let e: any = document.createElement("div");
e.innerHTML = html;
// 关键:在解码后添加样式处理
const detailAllElements = e.querySelectorAll(".o_detail_all");
detailAllElements.forEach((detailAll: HTMLElement) => {
// 为文字类子元素添加居中样式
let textElements: any = [
...Array.from(detailAll.querySelectorAll<HTMLElement>(".o_detail_text")),
...Array.from(detailAll.querySelectorAll<HTMLElement>(".o_detail_small")),
...Array.from(detailAll.querySelectorAll<HTMLElement>(".o_detail_title"))
];
textElements.forEach((el: any) => {
// 保留原有样式,追加居中样式(避免覆盖已有样式)
el.style.textAlign = "center";
// 如果需要强制覆盖,可添加 !important
// el.style.textAlign = 'center !important';
});
});
if (e.childNodes.length > 1) {
return e.innerHTML;
} else {
return e.childNodes[0].innerHTML;
}
};
//将参数分离 //将参数分离
export const initDetailParams = (dataStore: any, data: any, editorRef: any) => { export const initDetailParams = (dataStore: any, data: any, editorRef: any) => {
let is = hasIdRecursive(dataStore.options, data.category_id); let is = hasIdRecursive(dataStore.options, data.category_id);
@@ -40,14 +69,14 @@ export const initDetailParams = (dataStore: any, data: any, editorRef: any) => {
stock_qty: data.stock_qty, stock_qty: data.stock_qty,
id: data.id id: data.id
}); });
console.log(data.detail, "=======detail========");
//详情 //详情
if (!data.detail) { if (!data.detail) {
dataStore.detail = ""; dataStore.detail = "";
editorRef?.value?.clearEditor(); // 调用子组件的清空方法 editorRef?.value?.clearEditor(); // 调用子组件的清空方法
} else { } else {
dataStore.detail = data.detail; dataStore.detail = htmlDecode(data.detail); //htmlDecode(data.detail);
console.log(data.detail, "=======detail========");
} }
//图片 //图片