Ver código fonte

旅游推荐优化

wengan01 1 semana atrás
pai
commit
af613e74cf
1 arquivos alterados com 191 adições e 111 exclusões
  1. 191 111
      src/components/Editor/index.vue

+ 191 - 111
src/components/Editor/index.vue

@@ -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; /* 强制换行 */