|
|
@@ -65,17 +65,27 @@ import { globalHeaders } from '@/utils/request';
|
|
|
// 富文本统一输出内联样式:Quill默认输出class名(如ql-size-huge、ql-align-center),
|
|
|
// 只有加载了Quill样式表的页面才能渲染,APP端渲染器不认识会导致字体大小/对齐丢失
|
|
|
const SizeAttributor = Quill.import('attributors/style/size') as any;
|
|
|
-SizeAttributor.whitelist = ['10px', '12px', '14px', '16px', '18px', '20px', '24px', '32px'];
|
|
|
+SizeAttributor.whitelist = ['10px', '12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px', '36px', '40px', '48px'];
|
|
|
Quill.register(SizeAttributor, true);
|
|
|
Quill.register(Quill.import('attributors/style/align') as any, true);
|
|
|
Quill.register(Quill.import('attributors/style/direction') as any, true);
|
|
|
|
|
|
-// 缩进回退为内联padding-left(整段平移):字符式(行首U+00A0)首行缩进会被APP端渲染组件
|
|
|
-// 裁剪文本节点首尾空白时裁掉,到不了屏幕;padding-left是APP唯一能渲染的缩进样式,
|
|
|
-// 且编辑器显示与APP完全一致,两端所见即所得。
|
|
|
-// Quill默认缩进输出class(ql-indent-N),这里用自定义attributor把缩进层级承载为padding-left
|
|
|
-const INDENT_EM = 2; // 每级缩进对应的em数
|
|
|
+// 字体下拉:输出内联style font-family(Quill默认是ql-font-* class,APP端不认);
|
|
|
+// 下拉选项的中文名/字体预览由下方全局CSS的 ::before 提供
|
|
|
+// 注意:工具栏配置里必须显式传这份列表,传空数组Quill会用内置的serif/monospace默认项
|
|
|
+const FONT_LIST = ['SimSun', 'SimHei', 'KaiTi', 'FangSong', 'Microsoft YaHei', 'Arial', 'Times New Roman'];
|
|
|
+const FontAttributor = Quill.import('attributors/style/font') as any;
|
|
|
+FontAttributor.whitelist = FONT_LIST;
|
|
|
+Quill.register(FontAttributor, true);
|
|
|
+
|
|
|
+// 缩进最终方案:行首U+00A0字符式首行缩进。APP端是WebView(标准浏览器内核),不间断空格
|
|
|
+// 会被原样保留渲染,编辑器显示与APP完全一致(所见即所得);
|
|
|
+// 普通空格/全角空格按HTML规则折叠,保存时统一转成不间断空格。
|
|
|
+// Quill默认缩进输出class(ql-indent-N),APP不认识:Tab键等走下面attributor兜底承载为
|
|
|
+// padding-left,保存/加载时由migrateIndentHtml统一转回行首不间断空格
|
|
|
+const INDENT_EM = 2; // 每级缩进对应的em数(padding-left换算用)
|
|
|
const INDENT_MAX = 8; // 与Quill默认缩进层级上限保持一致
|
|
|
+const INDENT_CHARS = 2; // 每级缩进对应的不间断空格数(首行缩进2字符)
|
|
|
const StyleAttributorBase: any = (Quill.import('parchment') as any).StyleAttributor;
|
|
|
class IndentStyleAttributor extends StyleAttributorBase {
|
|
|
add(node: HTMLElement, value: any) {
|
|
|
@@ -159,9 +169,10 @@ const TOOLBAR_TIPS: [string, string][] = [
|
|
|
['button.ql-code-block', '代码块:选中内容设为代码块'],
|
|
|
['button.ql-list[value="ordered"]', '有序列表:选中内容加数字编号'],
|
|
|
['button.ql-list[value="bullet"]', '无序列表:选中内容加圆点符号'],
|
|
|
- ['button.ql-indent[value="-1"]', '减少缩进:降低段落缩进层级(APP端同步生效)'],
|
|
|
- ['button.ql-indent[value="+1"]', '增加缩进:提高段落缩进层级(APP端同步生效)'],
|
|
|
+ ['button.ql-indent[value="-1"]', '减少缩进:删除段首2个空格缩进(APP端同步显示)'],
|
|
|
+ ['button.ql-indent[value="+1"]', '增加缩进:段首添加2个空格缩进(APP端同步显示)'],
|
|
|
['.ql-picker.ql-size .ql-picker-label', '字体大小:设置选中文字的字号'],
|
|
|
+ ['.ql-picker.ql-font .ql-picker-label', '字体:下拉选择字体(部分字体依赖手机系统是否安装)'],
|
|
|
['.ql-picker.ql-header .ql-picker-label', '标题:切换段落标题级别'],
|
|
|
['.ql-picker.ql-color .ql-picker-label', '字体颜色:设置选中文字的颜色'],
|
|
|
['.ql-picker.ql-background .ql-picker-label', '背景颜色:设置选中文字的背景色'],
|
|
|
@@ -283,8 +294,9 @@ const options = ref<any>({
|
|
|
['blockquote', 'code-block'], // 引用 代码块
|
|
|
[{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
|
|
|
[{ indent: '-1' }, { indent: '+1' }], // 缩进
|
|
|
- [{ size: ['10px', false, '18px', '32px'] }], // 字体大小(内联样式模式)
|
|
|
- [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
|
|
|
+ [{ size: ['12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px', '36px', '40px', '48px'] }], // 字体大小(内联样式,平滑递增档位)
|
|
|
+ [{ font: FONT_LIST }], // 字体(Word式换字体下拉,输出内联font-family)
|
|
|
+ [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题(h1~h6+文本)
|
|
|
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
|
|
[{ align: [] }], // 对齐方式
|
|
|
['clean'], // 清除文本格式
|
|
|
@@ -316,6 +328,20 @@ const options = ref<any>({
|
|
|
videoDialog.uploadedName = '';
|
|
|
videoDialog.mode = 'url';
|
|
|
videoDialog.visible = true;
|
|
|
+ },
|
|
|
+ // 缩进按钮:直接操作段首不间断空格(首行缩进),不走Quill的indent格式(整段平移)
|
|
|
+ indent: (value: string) => {
|
|
|
+ applyIndent(value);
|
|
|
+ },
|
|
|
+ // 标题:先清掉行内字号,否则之前手动设过的字号会盖住标题自带字号
|
|
|
+ // (表现为换了标题级别却“没啥变化”)
|
|
|
+ header: (value: number | false) => {
|
|
|
+ const quill = toRaw(quillEditorRef.value)?.getQuill();
|
|
|
+ if (!quill) return;
|
|
|
+ if (value) {
|
|
|
+ quill.format('size', false);
|
|
|
+ }
|
|
|
+ quill.format('header', value || false);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
@@ -347,13 +373,13 @@ watch(
|
|
|
() => props.modelValue,
|
|
|
(v: string) => {
|
|
|
if (v === lastEmitted) return;
|
|
|
- // 旧数据缩进可能是Quill class(ql-indent-N)或上一版行首不间断空格,统一归一到padding-left内联样式;
|
|
|
- // 再把手工敲的行内空格统一成不间断空格,保证不编辑直接保存也能在APP生效。
|
|
|
+ // 旧数据缩进可能是Quill class(ql-indent-N)或padding-left内联样式(旧方案),统一转成行首不间断空格;
|
|
|
+ // 再把手工敲的行首/行内空格统一成不间断空格,保证编辑器显示与APP一致。
|
|
|
// 转换在watch渲染期同步执行,一旦对畸形/超大内容抛错会连累整个Editor挂载失败、
|
|
|
// 弹窗打不开(表现为点"修改"没反应),所以异常时降级为原始内容,绝不阻断渲染
|
|
|
let next: string;
|
|
|
try {
|
|
|
- next = spacesToPaddingHtml(stabilizeSpacesHtml(migrateIndentHtml(v || '')));
|
|
|
+ next = stabilizeSpacesHtml(migrateIndentHtml(v || ''));
|
|
|
} catch (e) {
|
|
|
console.error('[Editor] 内容转换失败,降级为原始内容:', e);
|
|
|
next = v || '';
|
|
|
@@ -534,10 +560,11 @@ function cleanBackgroundOnly(parent: HTMLElement) {
|
|
|
// 手工空格缩进也要在APP生效:HTML默认折叠行首和连续空格,编辑器靠white-space:pre-wrap
|
|
|
// 才能原样显示,APP端渲染器按标准HTML折叠,用户自己敲的空格就失效了。
|
|
|
// 输出时把每行行首空白、行内连续空白换成不间断空格(U+00A0),任何渲染器都原样保留
|
|
|
-const SPACE_RISK_RE = /(^|>)[ \t]|[ \t]{2,}/;
|
|
|
+const SPACE_RISK_RE = /(^|>)[ \t]|[ \t\u3000]{2,}|\u3000/;
|
|
|
function stabilizeSpacesHtml(html: string): string {
|
|
|
if (!html || !SPACE_RISK_RE.test(html)) return html;
|
|
|
- let out = html;
|
|
|
+ // 全角空格(中文输入法敲的)也会被折叠,统一换成2个不间断空格
|
|
|
+ let out = html.replace(/\u3000/g, '\u00a0\u00a0');
|
|
|
// 1) 段落行首空白:中间允许隔内联开标签或已转换的不间断空格,多轮收敛
|
|
|
const leadRe = /(<(?:p|div|h[1-6]|li|blockquote|pre)\b[^>]*>(?:\u00a0|<(?!br\b|img\b|video\b|\/)[^>]+>)*)([ \t]+)/g;
|
|
|
for (let pass = 0; pass < 4; pass++) {
|
|
|
@@ -555,47 +582,82 @@ function stabilizeSpacesHtml(html: string): string {
|
|
|
return parts.join('');
|
|
|
}
|
|
|
|
|
|
-// 行首空白缩进归一到padding-left:APP端渲染组件会裁掉文本节点首尾空白(U+00A0也被裁),
|
|
|
-// 字符式缩进到不了屏幕;块级标签行首的连续空白(2字符=1级)在这里并入padding-left内联样式,
|
|
|
-// 与缩进按钮同一载体,APP端才能渲染出来
|
|
|
-const LEAD_RUN_RE = /(<(?:p|div|h[1-6]|li|blockquote|pre)\b[^>]*>(?:<(?!br\b|img\b|video\b|\/)[^>]+>)*)([ \t\u00a0]{2,})/gi;
|
|
|
-function spacesToPaddingHtml(html: string): string {
|
|
|
+// 历史缩进标记统一转成行首不间断空格:ql-indent-N class(Quill默认输出)与padding-left内联
|
|
|
+// 样式(旧方案/Tab兕底)都换算成段首2×层级个U+00A0;行首已有空白时两者取大不累加,
|
|
|
+// 保证反复加载/保存幂等。WebView对U+00A0原样渲染,编辑器(pre-wrap)显示与APP完全一致
|
|
|
+const OPEN_TAG_LEAD_RE = /<(p|div|h[1-6]|li|blockquote|pre)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([ \t\u00a0\u3000]*)/gi;
|
|
|
+function migrateIndentHtml(html: string): string {
|
|
|
if (!html || html.indexOf('>') < 0) return html;
|
|
|
- return html.replace(LEAD_RUN_RE, (m: string, head: string, run: string) => {
|
|
|
- const level = Math.floor(run.length / 2);
|
|
|
- if (level < 1) return m;
|
|
|
- return addPaddingLevel(head, level);
|
|
|
+ return html.replace(OPEN_TAG_LEAD_RE, (tag: string, name: string, attrs: string, lead: string) => {
|
|
|
+ let level = 0;
|
|
|
+ let newAttrs = attrs;
|
|
|
+ const cm = /\sclass\s*=\s*"([^"]*)"/i.exec(newAttrs) || /\sclass\s*=\s*'([^']*)'/i.exec(newAttrs);
|
|
|
+ if (cm) {
|
|
|
+ const im = /ql-indent-(\d+)/.exec(cm[1]);
|
|
|
+ if (im) {
|
|
|
+ level = Math.min(parseInt(im[1], 10) || 0, INDENT_MAX);
|
|
|
+ const rest = cm[1]
|
|
|
+ .replace(/ql-indent-\d+/g, '')
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
+ .trim();
|
|
|
+ newAttrs = newAttrs.replace(cm[0], rest ? ` class="${rest}"` : '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const sm = /\sstyle\s*=\s*"([^"]*)"/i.exec(newAttrs) || /\sstyle\s*=\s*'([^']*)'/i.exec(newAttrs);
|
|
|
+ if (sm) {
|
|
|
+ const pm = /padding-left\s*:\s*([^;"]+)/i.exec(sm[1]);
|
|
|
+ if (pm) {
|
|
|
+ const raw = pm[1].trim();
|
|
|
+ const n = parseFloat(raw);
|
|
|
+ if (n > 0) {
|
|
|
+ const lv = Math.round(raw.toLowerCase().indexOf('px') >= 0 ? n / (INDENT_EM * 16) : n / INDENT_EM);
|
|
|
+ level = Math.max(level, Math.min(lv, INDENT_MAX));
|
|
|
+ }
|
|
|
+ const rest = sm[1]
|
|
|
+ .replace(/padding-left\s*:\s*[^;"]+;?/gi, '')
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
+ .replace(/^;+/, '')
|
|
|
+ .trim();
|
|
|
+ newAttrs = newAttrs.replace(sm[0], rest ? ` style="${rest}"` : '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 全角空格、Tab按2字符宽度计
|
|
|
+ let leadWidth = 0;
|
|
|
+ for (const ch of lead) leadWidth += ch === '\t' || ch === '\u3000' ? 2 : 1;
|
|
|
+ if (level <= 0 && leadWidth <= 0) return tag;
|
|
|
+ const want = Math.min(Math.max(level * INDENT_CHARS, leadWidth), INDENT_MAX * INDENT_CHARS);
|
|
|
+ return `<${name}${newAttrs}>` + '\u00a0'.repeat(want);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-// 给块级开标签叠加padding-left层级:已有padding-left时累加,上限INDENT_MAX
|
|
|
-function addPaddingLevel(tag: string, addLevel: number): string {
|
|
|
- const sm = /\sstyle\s*=\s*"([^"]*)"/i.exec(tag) || /\sstyle\s*=\s*'([^']*)'/i.exec(tag);
|
|
|
- let level = addLevel;
|
|
|
- if (sm) {
|
|
|
- const pm = /padding-left\s*:\s*([^;"]+)/i.exec(sm[1]);
|
|
|
- if (pm) {
|
|
|
- const raw = pm[1].trim();
|
|
|
- const n = parseFloat(raw);
|
|
|
- if (n > 0) level += Math.round(raw.toLowerCase().indexOf('px') >= 0 ? n / (INDENT_EM * 16) : n / INDENT_EM);
|
|
|
+// 缩进按钮:每段段首加/删2个不间断空格(首行缩进,编辑器与APP渲染一致)
|
|
|
+function applyIndent(direction: string) {
|
|
|
+ const quill = toRaw(quillEditorRef.value)?.getQuill();
|
|
|
+ if (!quill) return;
|
|
|
+ const range = quill.getSelection(true);
|
|
|
+ if (!range) return;
|
|
|
+ const lines = quill.getLines(range.index, range.length) as any[];
|
|
|
+ if (!lines.length) return;
|
|
|
+ // 从最后一行往上处理,避免增删导致偏移漂移
|
|
|
+ for (let i = lines.length - 1; i >= 0; i--) {
|
|
|
+ const line = lines[i];
|
|
|
+ const offset = quill.getIndex(line);
|
|
|
+ const text: string = typeof line.text === 'function' ? line.text() : '';
|
|
|
+ if (direction === '+1') {
|
|
|
+ quill.insertText(offset, '\u00a0\u00a0', 'user');
|
|
|
+ } else {
|
|
|
+ let remove = 0;
|
|
|
+ while (remove < 2 && remove < text.length && /[\u00a0 \t]/.test(text[remove])) remove++;
|
|
|
+ if (remove > 0) quill.deleteText(offset, remove, 'user');
|
|
|
}
|
|
|
- level = Math.min(level, INDENT_MAX);
|
|
|
- const rest = sm[1]
|
|
|
- .replace(/padding-left\s*:\s*[^;"]+;?/gi, '')
|
|
|
- .replace(/\s+/g, ' ')
|
|
|
- .replace(/^;+/, '')
|
|
|
- .trim();
|
|
|
- return tag.replace(sm[0], ` style="padding-left: ${level * INDENT_EM}em;${rest ? ` ${rest}` : ''}"`);
|
|
|
}
|
|
|
- level = Math.min(level, INDENT_MAX);
|
|
|
- return tag.slice(0, -1) + ` style="padding-left: ${level * INDENT_EM}em;">`;
|
|
|
}
|
|
|
|
|
|
-// 内容变化统一出口:先做"空格缩进APP化"转换再发给父组件
|
|
|
+// 内容变化统一出口:先做缩进归一+空格稳定转换再发给父组件
|
|
|
function handleTextChange() {
|
|
|
// 同watch:转换抛错时降级为原始内容,保证编辑/保存链路不因转换异常中断
|
|
|
try {
|
|
|
- lastEmitted = spacesToPaddingHtml(stabilizeSpacesHtml(content.value));
|
|
|
+ lastEmitted = stabilizeSpacesHtml(migrateIndentHtml(content.value));
|
|
|
} catch (e) {
|
|
|
console.error('[Editor] 内容转换失败,降级为原始内容:', e);
|
|
|
lastEmitted = content.value;
|
|
|
@@ -603,46 +665,6 @@ function handleTextChange() {
|
|
|
emit('update:modelValue', lastEmitted);
|
|
|
}
|
|
|
|
|
|
-// 历史数据兼容:旧版缩进是Quill class(ql-indent-N),加载时转成内联padding-left;
|
|
|
-// 上一版行首不间断空格式缩进由spacesToPaddingHtml归一
|
|
|
-function migrateIndentHtml(html: string): string {
|
|
|
- if (!html || html.indexOf('ql-indent-') < 0) return html;
|
|
|
- return html.replace(/<([a-zA-Z][\w-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g, (tag: string, name: string, attrs: string) => {
|
|
|
- const cm = /\sclass\s*=\s*"([^"]*)"/i.exec(attrs) || /\sclass\s*=\s*'([^']*)'/i.exec(attrs);
|
|
|
- if (!cm) return tag;
|
|
|
- const im = /ql-indent-(\d+)/.exec(cm[1]);
|
|
|
- if (!im) return tag;
|
|
|
- const level = Math.min(parseInt(im[1], 10) || 0, INDENT_MAX);
|
|
|
- if (level < 1) return tag;
|
|
|
- const restClass = cm[1]
|
|
|
- .replace(/ql-indent-\d+/g, '')
|
|
|
- .replace(/\s+/g, ' ')
|
|
|
- .trim();
|
|
|
- let newAttrs = attrs.replace(cm[0], restClass ? ` class="${restClass}"` : '');
|
|
|
- const pad = `padding-left: ${level * INDENT_EM}em;`;
|
|
|
- const sm = /\sstyle\s*=\s*"([^"]*)"/i.exec(newAttrs);
|
|
|
- if (sm) {
|
|
|
- newAttrs = newAttrs.replace(sm[0], ` style="${pad}${sm[1]}"`);
|
|
|
- } else {
|
|
|
- newAttrs += ` style="${pad}"`;
|
|
|
- }
|
|
|
- return `<${name}${newAttrs}>`;
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 读缩进层级:兼容内联padding-left与旧版ql-indent-N class(粘贴内容用)
|
|
|
-function readIndentLevel(el: HTMLElement): number {
|
|
|
- let level = 0;
|
|
|
- const cm = /ql-indent-(\d+)/.exec(el.getAttribute('class') || '');
|
|
|
- if (cm) level = parseInt(cm[1], 10) || 0;
|
|
|
- const pl = el.style.paddingLeft;
|
|
|
- if (pl) {
|
|
|
- const n = parseFloat(pl);
|
|
|
- if (n > 0) level = Math.max(level, Math.round(pl.indexOf('px') >= 0 ? n / (INDENT_EM * 16) : n / INDENT_EM));
|
|
|
- }
|
|
|
- return level;
|
|
|
-}
|
|
|
-
|
|
|
// 插入一个带块级格式的换行符:Quill的缩进/对齐/标题/列表都是"行"格式,只认换行符上的属性
|
|
|
function insertBlockBreak(delta: Delta, el: HTMLElement, listType: 'ordered' | 'bullet' | null) {
|
|
|
const fmt: { [key: string]: any } = {};
|
|
|
@@ -650,8 +672,6 @@ function insertBlockBreak(delta: Delta, el: HTMLElement, listType: 'ordered' | '
|
|
|
if (hm) fmt.header = parseInt(hm[1], 10);
|
|
|
if (el.style.textAlign) fmt.align = el.style.textAlign;
|
|
|
if (listType) fmt.list = listType;
|
|
|
- const level = readIndentLevel(el);
|
|
|
- if (level > 0) fmt.indent = level;
|
|
|
delta.insert('\n', Object.keys(fmt).length ? fmt : undefined);
|
|
|
}
|
|
|
|
|
|
@@ -846,6 +866,48 @@ onMounted(() => {
|
|
|
.quill-img {
|
|
|
display: none;
|
|
|
}
|
|
|
+/* 字体下拉:选项显示中文名并用对应字体预览(Word式);未选中时标签显示“字体” */
|
|
|
+.ql-snow .ql-picker.ql-font {
|
|
|
+ width: 108px;
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label::before {
|
|
|
+ content: '字体';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='SimSun']::before,
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='SimSun']::before {
|
|
|
+ content: '宋体';
|
|
|
+ font-family: 'SimSun';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='SimHei']::before,
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='SimHei']::before {
|
|
|
+ content: '黑体';
|
|
|
+ font-family: 'SimHei';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='KaiTi']::before,
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='KaiTi']::before {
|
|
|
+ content: '楷体';
|
|
|
+ font-family: 'KaiTi';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='FangSong']::before,
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='FangSong']::before {
|
|
|
+ content: '仿宋';
|
|
|
+ font-family: 'FangSong';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='Microsoft YaHei']::before,
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='Microsoft YaHei']::before {
|
|
|
+ content: '微软雅黑';
|
|
|
+ font-family: 'Microsoft YaHei';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='Arial']::before,
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='Arial']::before {
|
|
|
+ content: 'Arial';
|
|
|
+ font-family: 'Arial';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='Times New Roman']::before,
|
|
|
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='Times New Roman']::before {
|
|
|
+ content: 'Times New Roman';
|
|
|
+ font-family: 'Times New Roman';
|
|
|
+}
|
|
|
.ql-snow .ql-tooltip[data-mode='link']::before {
|
|
|
content: '请输入链接地址:';
|
|
|
}
|
|
|
@@ -857,34 +919,63 @@ onMounted(() => {
|
|
|
.ql-snow .ql-tooltip[data-mode='video']::before {
|
|
|
content: '请输入视频地址:';
|
|
|
}
|
|
|
+/* 字号下拉:每一档都要有显示文字规则,否则兜底显示成“14px” */
|
|
|
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
|
|
|
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
|
|
|
- content: '14px';
|
|
|
+ content: '字号';
|
|
|
}
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='10px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='10px']::before {
|
|
|
content: '10px';
|
|
|
}
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
|
|
|
- content: '18px';
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='12px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='12px']::before {
|
|
|
+ content: '12px';
|
|
|
}
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
|
|
|
- content: '32px';
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='14px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='14px']::before {
|
|
|
+ content: '14px';
|
|
|
}
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='10px']::before,
|
|
|
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='10px']::before {
|
|
|
- content: '10px';
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='16px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='16px']::before {
|
|
|
+ content: '16px';
|
|
|
}
|
|
|
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='18px']::before,
|
|
|
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='18px']::before {
|
|
|
content: '18px';
|
|
|
}
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='20px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='20px']::before {
|
|
|
+ content: '20px';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='22px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='22px']::before {
|
|
|
+ content: '22px';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='24px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='24px']::before {
|
|
|
+ content: '24px';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='28px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='28px']::before {
|
|
|
+ content: '28px';
|
|
|
+}
|
|
|
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='32px']::before,
|
|
|
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='32px']::before {
|
|
|
content: '32px';
|
|
|
}
|
|
|
+.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 {
|
|
|
+ content: '36px';
|
|
|
+}
|
|
|
+.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 {
|
|
|
+ content: '40px';
|
|
|
+}
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='48px']::before,
|
|
|
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='48px']::before {
|
|
|
+ content: '48px';
|
|
|
+}
|
|
|
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
|
|
|
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
|
|
|
content: '文本';
|
|
|
@@ -913,18 +1004,7 @@ onMounted(() => {
|
|
|
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
|
|
|
content: '标题6';
|
|
|
}
|
|
|
-.ql-snow .ql-picker.ql-font .ql-picker-label::before,
|
|
|
-.ql-snow .ql-picker.ql-font .ql-picker-item::before {
|
|
|
- content: '标准字体';
|
|
|
-}
|
|
|
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
|
|
|
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
|
|
|
- content: '衬线字体';
|
|
|
-}
|
|
|
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
|
|
|
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
|
|
|
- content: '等宽字体';
|
|
|
-}
|
|
|
+/* 字体下拉默认标签与选项文字由上方 SimSun 等规则提供;serif/monospace 不在白名单,旧规则已删 */
|
|
|
|
|
|
.editor .ql-editor {
|
|
|
white-space: pre-wrap; /* 强制换行 */
|