|
|
@@ -88,6 +88,10 @@ const options = ref<any>({
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ },
|
|
|
+ clipboard: {
|
|
|
+ matchVisual: false, // 关闭视觉换行
|
|
|
+ matchers: [] // 清空所有匹配器
|
|
|
}
|
|
|
},
|
|
|
placeholder: '请输入内容',
|
|
|
@@ -155,11 +159,233 @@ const handleBeforeUpload = (file: any) => {
|
|
|
proxy?.$modal.loading('正在上传文件,请稍候...');
|
|
|
return true;
|
|
|
};
|
|
|
-
|
|
|
// 图片失败拦截
|
|
|
const handleUploadError = (err: any) => {
|
|
|
proxy?.$modal.msgError('上传文件失败');
|
|
|
};
|
|
|
+import { onMounted, nextTick } from 'vue';
|
|
|
+import Delta from 'quill-delta';
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await nextTick();
|
|
|
+ const quill = quillEditorRef.value?.getQuill();
|
|
|
+ if (!quill) return;
|
|
|
+
|
|
|
+ // 清空默认行为
|
|
|
+ quill.clipboard.matchers = [];
|
|
|
+
|
|
|
+ quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node: HTMLElement, delta: Delta) => {
|
|
|
+ const tempDiv = document.createElement('div');
|
|
|
+ tempDiv.innerHTML = node.outerHTML;
|
|
|
+
|
|
|
+ // ✅ 只清除 background 相关样式,保留 color
|
|
|
+ cleanBackgroundOnly(tempDiv);
|
|
|
+
|
|
|
+ // 构建 Delta(支持格式 + 列表)
|
|
|
+ const newDelta = new Delta();
|
|
|
+ buildDeltaWithListSupport(tempDiv, newDelta, {}, null);
|
|
|
+ return newDelta;
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// ✅ 只清除 background,保留 color/font-size 等
|
|
|
+function cleanBackgroundOnly(parent: HTMLElement) {
|
|
|
+ if (parent.style) {
|
|
|
+ parent.style.backgroundColor = '';
|
|
|
+ parent.style.background = '';
|
|
|
+ parent.style.color = '';
|
|
|
+ parent.style.backgroundImage = '';
|
|
|
+ parent.style.backgroundPosition = '';
|
|
|
+ parent.style.backgroundRepeat = '';
|
|
|
+ parent.style.backgroundSize = '';
|
|
|
+ parent.style.backgroundAttachment = '';
|
|
|
+ // ✅ 不动 color、font、size 等
|
|
|
+ }
|
|
|
+
|
|
|
+ Array.from(parent.children).forEach((child) => {
|
|
|
+ if (child instanceof HTMLElement) {
|
|
|
+ cleanBackgroundOnly(child);
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// ✅ 支持列表的 Delta 构建
|
|
|
+function buildDeltaWithListSupport(
|
|
|
+ node: Node,
|
|
|
+ delta: Delta,
|
|
|
+ formatStack: { [key: string]: any },
|
|
|
+ listType: 'ordered' | 'bullet' | null // 当前是否在列表中
|
|
|
+) {
|
|
|
+ if (node.nodeType === Node.TEXT_NODE) {
|
|
|
+ const text = node.textContent || '';
|
|
|
+ if (text.trim() || text === ' ') {
|
|
|
+ delta.insert(text, formatStack);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
|
+
|
|
|
+ const el = node as HTMLElement;
|
|
|
+ const tagName = el.tagName.toLowerCase();
|
|
|
+
|
|
|
+ const currentFormat = { ...formatStack };
|
|
|
+ let newListType: 'ordered' | 'bullet' | null = listType;
|
|
|
+
|
|
|
+ // 处理列表开始
|
|
|
+ if (tagName === 'ol') {
|
|
|
+ newListType = 'ordered';
|
|
|
+ }
|
|
|
+ if (tagName === 'ul') {
|
|
|
+ newListType = 'bullet';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理列表项
|
|
|
+ if (tagName === 'li') {
|
|
|
+ if (listType) {
|
|
|
+ // 标记为列表项
|
|
|
+ currentFormat.list = listType;
|
|
|
+ }
|
|
|
+ // 如果 li 有嵌套 ol/ul,子项可能改变类型,但这里简化处理
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加内联格式
|
|
|
+ if (tagName === 'strong' || tagName === 'b') {
|
|
|
+ currentFormat.bold = true;
|
|
|
+ }
|
|
|
+ if (tagName === 'em' || tagName === 'i') {
|
|
|
+ currentFormat.italic = true;
|
|
|
+ }
|
|
|
+ if (tagName === 'u' || el.style.textDecoration === 'underline') {
|
|
|
+ currentFormat.underline = true;
|
|
|
+ }
|
|
|
+ if (tagName === 's' || tagName === 'strike') {
|
|
|
+ currentFormat.strike = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ✅ 保留颜色(关键!)
|
|
|
+ if (el.style.color) {
|
|
|
+ currentFormat.color = el.style.color;
|
|
|
+ }
|
|
|
+ if (el.style.fontSize) {
|
|
|
+ currentFormat.size = el.style.fontSize; // 注意:Quill 的 size 是 small/large/huge 或值
|
|
|
+ }
|
|
|
+ if (el.style.fontFamily) {
|
|
|
+ currentFormat.font = el.style.fontFamily.split(',')[0].trim().replace(/['"]/g, '');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 是否是块级元素(需要换行)
|
|
|
+ const isBlock = ['p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'br'].includes(tagName);
|
|
|
+
|
|
|
+ // 遍历子节点
|
|
|
+ for (let i = 0; i < el.childNodes.length; i++) {
|
|
|
+ const child = el.childNodes[i];
|
|
|
+
|
|
|
+ if (child.nodeType === Node.TEXT_NODE && isBlock && i === 0) {
|
|
|
+ // 块级元素开头插入换行(除了第一个)
|
|
|
+ if (delta.length() > 0) {
|
|
|
+ delta.insert('\n');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ buildDeltaWithListSupport(child, delta, currentFormat, newListType);
|
|
|
+
|
|
|
+ // 如果是块级元素且不是最后一个,加换行
|
|
|
+ if (isBlock && i === el.childNodes.length - 1 && (el.nextSibling || el.parentNode !== el.ownerDocument?.body)) {
|
|
|
+ if (!delta.ops.length || delta.ops[delta.ops.length - 1].insert !== '\n') {
|
|
|
+ delta.insert('\n');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+/*
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await nextTick();
|
|
|
+ const quill = quillEditorRef.value?.getQuill();
|
|
|
+ if (!quill) return;
|
|
|
+
|
|
|
+ // ✅ 1. 移除所有默认的 matcher,避免干扰
|
|
|
+ quill.clipboard.matchers = [];
|
|
|
+
|
|
|
+ // ✅ 2. 添加自定义 matcher:对所有元素节点,只提取 innerText
|
|
|
+ quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node: HTMLElement, delta: Delta) => {
|
|
|
+ // 获取纯文本
|
|
|
+ const text = node.innerText || node.textContent || '';
|
|
|
+
|
|
|
+ // 返回一个新的 Delta,只包含纯文本,无任何格式
|
|
|
+ return new Delta().insert(text);
|
|
|
+ });
|
|
|
+
|
|
|
+ // ✅ 3. 特别处理根节点是纯文本的情况(比如从记事本复制)
|
|
|
+ quill.clipboard.addMatcher(Node.TEXT_NODE, (node: Text, delta: Delta) => {
|
|
|
+ return new Delta().insert(node.data || '');
|
|
|
+ });
|
|
|
+
|
|
|
+ // ✅ 4. 可选:如果你希望保留换行,可以不做处理,Quill 会自动处理 p/br
|
|
|
+ // 如果你想更激进地清理,也可以在这里统一处理
|
|
|
+
|
|
|
+ console.log('[Clipboard] Custom matcher set, only plain text will be pasted.');
|
|
|
+});
|
|
|
+
|
|
|
+
|
|
|
+import { onMounted } from 'vue';
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ const quill = quillEditorRef.value.getQuill();
|
|
|
+ debugger;
|
|
|
+ // ✅ 2. 手动监听 paste 事件,完全由你控制
|
|
|
+ quill.root.addEventListener('paste', async (e) => {
|
|
|
+ e.preventDefault(); // ✅ 阻止浏览器默认行为
|
|
|
+
|
|
|
+ const clipboardData = e.clipboardData || (e as any).originalEvent.clipboardData;
|
|
|
+ const html = clipboardData.getData('text/html');
|
|
|
+ const text = clipboardData.getData('text/plain');
|
|
|
+
|
|
|
+ // 使用 HTML 优先,否则用纯文本
|
|
|
+ const tempDiv = document.createElement('div');
|
|
|
+ tempDiv.innerHTML = html || text;
|
|
|
+
|
|
|
+ // 清理所有样式和 class
|
|
|
+ const walk = (node: Node) => {
|
|
|
+ if (node.nodeType === Node.ELEMENT_NODE) {
|
|
|
+
|
|
|
+ const el = node as HTMLElement;
|
|
|
+ el.style.cssText = '';
|
|
|
+ el.removeAttribute('class');
|
|
|
+ el.removeAttribute('style');
|
|
|
+
|
|
|
+ // 特别清理 span 的内联样式
|
|
|
+ if (el.tagName === 'SPAN') {
|
|
|
+ el.style.color = '';
|
|
|
+ el.style.backgroundColor = '';
|
|
|
+ el.style.fontWeight = '';
|
|
|
+ el.style.fontStyle = '';
|
|
|
+ el.style.textDecoration = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ Array.from(el.children).forEach((child) => walk(child));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ Array.from(tempDiv.childNodes).forEach(walk);
|
|
|
+
|
|
|
+ const cleanHtml = tempDiv.innerHTML;
|
|
|
+ const range = quill.getSelection();
|
|
|
+ const index = range ? range.index : 0;
|
|
|
+ // ✅ 调试:打印关键信息
|
|
|
+ console.log('[Paste Debug]', { html, text, cleanHtml, index });
|
|
|
+
|
|
|
+ // ✅ 插入清理后的内容
|
|
|
+ quill.clipboard.dangerouslyPasteHTML(index, cleanHtml);
|
|
|
+
|
|
|
+ // 可选:将光标移到末尾
|
|
|
+ // setTimeout(() => {
|
|
|
+ // const length = quill.getLength();
|
|
|
+ // quill.setSelection(length, 0);
|
|
|
+ // }, 10);
|
|
|
+ });
|
|
|
+});*/
|
|
|
</script>
|
|
|
|
|
|
<style>
|