<!--

        <tinymce-text
          v-model="newData.data5"
          :height="300"
          :max-length="500"
          :upload-url="editorUploadUrl"
          :upload-headers="editorUploadHeaders"
        />

        import { getToken } from "@/utils/auth";
        import tinymceText from "@/components/tinymceContainer/tinymceText.vue";

        components: { tinymceText },

        editorUploadUrl: this.uploadUrl(),
        editorUploadHeaders: {
          Authorization:getToken(),
        },

-->
<template>
  <div class="tinymce-container">
    <Editor
      v-model="editorContent"
      :init="editorInit"
      :disabled="disabled"
    />

    <!-- 插入公式弹窗 -->
    <div v-if="latexDialogVisible" class="fm-mask" @click.self="latexDialogVisible = false">
      <div class="fm-dialog">
        <!-- 标题栏 -->
        <div class="fm-header">
          <span>插入公式</span>
          <span class="fm-close" @click="latexDialogVisible = false">×</span>
        </div>

        <!-- 工具栏 -->
        <div class="fm-toolbar">
          <!-- 预设下拉 -->
          <div class="fm-preset-wrap">
            <div class="fm-preset-btn" @click="presetOpen = !presetOpen">
              <span class="fm-preset-icon">f<sub>(x)</sub></span>
              <span class="fm-preset-label">预设</span>
              <span class="fm-preset-arrow">▾</span>
            </div>
            <div v-if="presetOpen" class="fm-preset-dropdown">
              <div v-for="p in presets" :key="p.label" class="fm-preset-item"
                   @click="applyPreset(p.value)">{{ p.label }}</div>
            </div>
          </div>

          <div class="fm-divider"></div>

          <!-- 常用符号面板 -->
          <div class="fm-symbol-grid">
            <button v-for="s in symbolList" :key="s.label" type="button"
                    class="fm-sym-btn" :title="s.tip" @click="insertAtCursor(s.value)">
              {{ s.label }}
            </button>
          </div>

          <div class="fm-divider"></div>

          <!-- 分类按钮组 -->
          <div class="fm-category-row">
            <div v-for="cat in categories" :key="cat.id" class="fm-cat-wrap">
              <button type="button" class="fm-cat-btn"
                      :class="{active: activeCat === cat.id}"
                      @click="activeCat = activeCat === cat.id ? null : cat.id">
                <span class="fm-cat-icon" v-html="cat.icon"></span>
                <span class="fm-cat-label">{{ cat.label }}</span>
                <span class="fm-cat-arrow">▾</span>
              </button>
              <div v-if="activeCat === cat.id" class="fm-cat-dropdown">
                <button v-for="item in cat.items" :key="item.label" type="button"
                        class="fm-cat-item" :title="item.tip"
                        @click="insertAtCursor(item.value); activeCat = null">
                  <span v-html="item.label"></span>
                </button>
              </div>
            </div>
          </div>
        </div>

        <!-- 输入区 -->
        <div class="fm-body">
          <textarea
            ref="fmInput"
            v-model="latexInput"
            class="fm-input"
            placeholder="在此处键入公式"
            spellcheck="false"
            @input="onInputChange"
          />
          <!-- 实时预览：由 renderPreview 方法直接操作 DOM，避免 MathJax 缓存问题 -->
          <div class="fm-preview" ref="fmPreview">
            <span class="fm-preview-empty">公式预览</span>
          </div>
        </div>

        <!-- 底部 -->
        <div class="fm-footer">
          <button class="fm-btn-close" type="button" @click="latexDialogVisible = false">关闭</button>
          <button class="fm-btn-save" type="button" @click="insertLatex">保存</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import Editor from '@tinymce/tinymce-vue'
  import tinymce from 'tinymce/tinymce'

  // 计算静态资源根路径，兼容 /bigWeb/ 或 / 等任意子目录部署
  // publicPath="./" 时，index.html 放在子目录下，tinymce 资源也在同级
  function getStaticBase() {
    const base = document.baseURI || window.location.href
    return base.replace(/#.*$/, '').replace(/\/[^/]*$/, '') + '/'
  }
  const staticBase = getStaticBase()

  import 'tinymce/themes/silver'
  import 'tinymce/icons/default'
  import 'tinymce/models/dom'
  import 'tinymce/plugins/advlist'
  import 'tinymce/plugins/autolink'
  import 'tinymce/plugins/autoresize'
  import 'tinymce/plugins/autosave'
  import 'tinymce/plugins/charmap'
  import 'tinymce/plugins/code'
  import 'tinymce/plugins/codesample'
  import 'tinymce/plugins/directionality'
  import 'tinymce/plugins/emoticons'
  import 'tinymce/plugins/fullscreen'
  import 'tinymce/plugins/help'
  import 'tinymce/plugins/image'
  import 'tinymce/plugins/importcss'
  import 'tinymce/plugins/insertdatetime'
  import 'tinymce/plugins/link'
  import 'tinymce/plugins/lists'
  import 'tinymce/plugins/media'
  import 'tinymce/plugins/nonbreaking'
  import 'tinymce/plugins/pagebreak'
  import 'tinymce/plugins/preview'
  import 'tinymce/plugins/quickbars'
  import 'tinymce/plugins/save'
  import 'tinymce/plugins/searchreplace'
  import 'tinymce/plugins/table'
  import 'tinymce/plugins/template'
  import 'tinymce/plugins/visualblocks'
  import 'tinymce/plugins/visualchars'
  import 'tinymce/plugins/wordcount'

  export default {
    name: 'TinymceContainer',
    components: { Editor },
    props: {
      value: { type: String, default: '' },
      disabled: { type: Boolean, default: false },
      height: { type: [Number, String], default: 400 },
      // 富文本纯文本内容最大长度，0 表示不限制
      maxLength: { type: Number, default: 0 },
      uploadUrl: { type: String, default: '' },
      uploadHeaders: { type: Object, default: () => ({}) }
    },
    data() {
      return {
        latexDialogVisible: false,
        latexInput: '',
        presetOpen: false,
        activeCat: null,
        // 预设模板
        presets: [
          { label: '二次方程', value: 'x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}' },
          { label: '勾股定理', value: 'a^2 + b^2 = c^2' },
          { label: '欧拉公式', value: 'e^{i\\pi} + 1 = 0' },
          { label: '正态分布', value: 'f(x)=\\frac{1}{\\sigma\\sqrt{2\\pi}}e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}' },
          { label: '泰勒展开', value: 'f(x) = \\sum_{n=0}^{\\infty} \\frac{f^{(n)}(a)}{n!}(x-a)^n' },
          { label: '矩阵行列式', value: '\\begin{vmatrix} a & b \\\\ c & d \\end{vmatrix} = ad - bc' },
        ],
        // 常用符号
        symbolList: [
          { label: '±', value: '\\pm ', tip: '正负号' },
          { label: '∞', value: '\\infty ', tip: '无穷' },
          { label: '=', value: '= ', tip: '等号' },
          { label: '~', value: '\\sim ', tip: '相似' },
          { label: '×', value: '\\times ', tip: '乘号' },
          { label: '÷', value: '\\div ', tip: '除号' },
          { label: '!', value: '! ', tip: '阶乘' },
          { label: '<', value: '< ', tip: '小于' },
          { label: '≪', value: '\\ll ', tip: '远小于' },
          { label: '>', value: '> ', tip: '大于' },
          { label: '≫', value: '\\gg ', tip: '远大于' },
          { label: '≤', value: '\\leq ', tip: '小于等于' },
          { label: '≥', value: '\\geq ', tip: '大于等于' },
          { label: '∓', value: '\\mp ', tip: '负正号' },
          { label: '≅', value: '\\cong ', tip: '全等' },
          { label: '≡', value: '\\equiv ', tip: '恒等' },
          { label: '∝', value: '\\propto ', tip: '正比于' },
          { label: '≈', value: '\\approx ', tip: '约等于' },
        ],
        // 分类按钮组
        categories: [
          {
            id: 'fraction', label: '分数', icon: '<i style="font-style:normal">x/y</i>',
            items: [
              { label: 'x/y', value: '\\frac{x}{y}', tip: '分数' },
              { label: 'x/y (斜)', value: '{}^{x}/{}_{y}', tip: '斜分数' },
              { label: 'x·y⁻¹', value: 'x \\cdot y^{-1}', tip: '倒数形式' },
            ]
          },
          {
            id: 'power', label: '上下标', icon: '<i style="font-style:normal">e<sup>x</sup></i>',
            items: [
              { label: 'x²', value: 'x^{2}', tip: '上标' },
              { label: 'xₙ', value: 'x_{n}', tip: '下标' },
              { label: 'xₙ²', value: 'x_{n}^{2}', tip: '上下标' },
              { label: 'eˣ', value: 'e^{x}', tip: 'e的幂次' },
            ]
          },
          {
            id: 'root', label: '根式', icon: '<i style="font-style:normal">ⁿ√x</i>',
            items: [
              { label: '√x', value: '\\sqrt{x}', tip: '平方根' },
              { label: 'ⁿ√x', value: '\\sqrt[n]{x}', tip: 'n次根' },
              { label: '∛x', value: '\\sqrt[3]{x}', tip: '立方根' },
            ]
          },
          {
            id: 'integral', label: '积分', icon: '<i style="font-style:normal">∫</i>',
            items: [
              { label: '∫', value: '\\int_{a}^{b} f(x)\\,dx', tip: '定积分' },
              { label: '∬', value: '\\iint_{D} f(x,y)\\,dx\\,dy', tip: '二重积分' },
              { label: '∭', value: '\\iiint_{V} f\\,dV', tip: '三重积分' },
              { label: '∮', value: '\\oint_{C} f\\,ds', tip: '曲线积分' },
            ]
          },
          {
            id: 'large', label: '大型运算符', icon: '<i style="font-style:normal">Σ</i>',
            items: [
              { label: 'Σ 求和', value: '\\sum_{i=1}^{n} x_i', tip: '求和' },
              { label: 'Π 连乘', value: '\\prod_{i=1}^{n} x_i', tip: '连乘积' },
              { label: 'lim 极限', value: '\\lim_{x \\to 0} f(x)', tip: '极限' },
              { label: 'max', value: '\\max_{x \\in D}', tip: '最大值' },
              { label: 'min', value: '\\min_{x \\in D}', tip: '最小值' },
            ]
          },
          {
            id: 'bracket', label: '括号', icon: '<i style="font-style:normal">{()}</i>',
            items: [
              { label: '()', value: '\\left( x \\right)', tip: '圆括号' },
              { label: '[]', value: '\\left[ x \\right]', tip: '方括号' },
              { label: '{}', value: '\\left\\{ x \\right\\}', tip: '花括号' },
              { label: '||', value: '\\left| x \\right|', tip: '绝对值' },
              { label: '‖‖', value: '\\left\\| x \\right\\|', tip: '范数' },
              { label: '矩阵()', value: '\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}', tip: '圆括号矩阵' },
              { label: '矩阵[]', value: '\\begin{bmatrix} a & b \\\\ c & d \\end{bmatrix}', tip: '方括号矩阵' },
            ]
          },
          {
            id: 'func', label: '函数', icon: '<i style="font-style:normal">sinθ</i>',
            items: [
              { label: 'sin', value: '\\sin\\theta', tip: '正弦' },
              { label: 'cos', value: '\\cos\\theta', tip: '余弦' },
              { label: 'tan', value: '\\tan\\theta', tip: '正切' },
              { label: 'ln', value: '\\ln x', tip: '自然对数' },
              { label: 'log', value: '\\log_{a} x', tip: '对数' },
              { label: 'lg', value: '\\lg x', tip: '常用对数' },
              { label: '∂', value: '\\frac{\\partial f}{\\partial x}', tip: '偏导数' },
              { label: "f'", value: "f'(x)", tip: '导数' },
            ]
          },
        ],
        editorInit: {
          language_url: staticBase + 'tinymce/langs/zh_CN.js',
          language: 'zh_CN',
          skin_url: staticBase + 'tinymce/skins/ui/oxide',
          content_css: staticBase + 'tinymce/skins/content/default/content.css',
          // 编辑区样式：公式 span 不可编辑，光标正常显示
          content_style: `
          .math-formula {
            display: inline-block;
            cursor: default;
            user-select: none;
            background: #f0f4ff;
            border-radius: 3px;
            padding: 1px 4px;
            font-size: 0.95em;
          }
        `,
          // 禁止 TinyMCE 把 data URI 图片自动上传（公式以 span 文本形式存储，不产生 data URI）
          automatic_uploads: false,
          images_reuse_filename: true,
          // 指定 emoticons 数据文件路径，避免动态加载时 404
          emoticons_database_url: staticBase + 'tinymce/plugins/emoticons/js/emojis.js',
          height: this.height,
          menubar: false,
          plugins: 'advlist autolink autoresize autosave charmap code codesample directionality emoticons fullscreen help image importcss insertdatetime link lists media nonbreaking pagebreak preview quickbars save searchreplace table template visualblocks visualchars wordcount',
          toolbar:
            'table | codesample | code | preview | fullscreen | image | media | attachment | ' +
            'fontsizeselect | forecolor | ' +
            'bold italic | ' +
            'alignleft aligncenter alignright alignjustify | ' +
            'bullist numlist | removeformat',
          toolbar_mode: 'wrap',
          quickbars_selection_toolbar: 'bold italic underline | quicklink h2 h3 blockquote',
          quickbars_insert_toolbar: false,
          // 插入/编辑图片弹窗：隐藏上传 tab，只保留 URL 输入
          image_uploadtab: false,
          // 插入/编辑媒体弹窗：只保留"一般"tab，隐藏内嵌和高级
          media_live_embeds: false,
          media_alt_source: false,
          media_poster: false,
          font_formats:
            '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;' +
            '宋体=SimSun,serif;黑体=SimHei,sans-serif;楷体=KaiTi,serif;' +
            '仿宋=FangSong,serif;Arial=arial,helvetica,sans-serif;' +
            'Times New Roman=times new roman,times,serif;',
          fontsize_formats: '12px 14px 16px 18px 20px 24px 28px 32px 36px',
          codesample_languages: [
            { text: 'HTML/XML', value: 'markup' },
            { text: 'JavaScript', value: 'javascript' },
            { text: 'CSS', value: 'css' },
            { text: 'Python', value: 'python' },
            { text: 'Java', value: 'java' },
            { text: 'C', value: 'c' },
            { text: 'C++', value: 'cpp' },
            { text: 'SQL', value: 'sql' },
          ],
          paste_data_images: true,
          autosave_interval: '30s',
          autosave_prefix: 'tinymce-autosave-{path}{query}-{id}-',
          autosave_restore_when_empty: false,
          images_upload_handler: (blobInfo) =>
            new Promise((resolve, reject) => {
              if (this.uploadUrl) {
                const xhr = new XMLHttpRequest()
                xhr.open('POST', this.uploadUrl)
                Object.keys(this.uploadHeaders).forEach(k => xhr.setRequestHeader(k, this.uploadHeaders[k]))
                xhr.onload = () => {
                  if (xhr.status < 200 || xhr.status >= 300) { reject('上传失败: ' + xhr.status); return }
                  try {
                    const res = JSON.parse(xhr.responseText)
                    // 兼容 { url } / { data: { url } } / { data: url }
                    const url = res.url || (res.data && (res.data.url || res.data)) || ''
                    if (url) resolve(url)
                    else reject('接口未返回图片地址')
                  } catch (e) { reject('接口响应解析失败') }
                }
                xhr.onerror = () => reject('网络错误')
                const fd = new FormData()
                fd.append('file', blobInfo.blob(), blobInfo.filename())
                xhr.send(fd)
              } else {
                const reader = new FileReader()
                reader.onload = () => resolve(reader.result)
                reader.onerror = () => reject('图片读取失败')
                reader.readAsDataURL(blobInfo.blob())
              }
            }),
          setup: (editor) => {
            // 字数限制：在 input 事件后检查纯文本长度
            editor.on('input keydown', (e) => {
              if (!this.maxLength) return
              // keydown 时只拦截非删除类按键
              if (e.type === 'keydown') {
                const allowKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']
                if (allowKeys.includes(e.key) || e.ctrlKey || e.metaKey) return
              }
              const text = editor.getContent({ format: 'text' })
              if (text.length >= this.maxLength) {
                if (e.type === 'keydown') {
                  e.preventDefault()
                  this.msgError(`内容长度不能超过 ${this.maxLength} 个字符`)
                }
              }
            })
            editor.ui.registry.addButton('latex', {
              text: '公式',
              tooltip: '插入公式',
              icon: 'insert-character',
              onAction: () => {
                this.latexInput = ''
                this.presetOpen = false
                this.activeCat = null
                this.latexDialogVisible = true
                this._editor = editor
              }
            })
            editor.ui.registry.addButton('attachment', {
              text: '',
              tooltip: '插入图片',
              icon: 'image',
              onAction: () => {
                const input = document.createElement('input')
                input.type = 'file'
                input.accept = 'image/jpeg,image/png,image/jpg'
                input.onchange = (e) => {
                  const file = e.target.files[0]
                  if (!file) return
                  // 类型校验
                  const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg']
                  if (!allowedTypes.includes(file.type)) {
                    alert('仅支持 jpg / jpeg / png 格式的图片')
                    return
                  }
                  // 大小校验：30MB
                  if (file.size > 30 * 1024 * 1024) {
                    alert('图片大小不能超过 30MB')
                    return
                  }
                  if (this.uploadUrl) {
                    const xhr = new XMLHttpRequest()
                    xhr.open('POST', this.uploadUrl)
                    Object.keys(this.uploadHeaders).forEach(k => xhr.setRequestHeader(k, this.uploadHeaders[k]))
                    xhr.onload = () => {
                      if (xhr.status < 200 || xhr.status >= 300) {
                        alert('上传失败: ' + xhr.status)
                        return
                      }
                      try {
                        const res = JSON.parse(xhr.responseText)
                        const url = res.url || (res.data && (res.data.url || res.data)) || ''
                        if (url) editor.insertContent(`<img src="${url}" alt="${file.name}" style="max-width:100%;" />&nbsp;`)
                        else alert('接口未返回图片地址')
                      } catch (err) { alert('接口响应解析失败') }
                    }
                    xhr.onerror = () => alert('网络错误，上传失败')
                    const fd = new FormData()
                    fd.append('file', file)
                    xhr.send(fd)
                  } else {
                    // 未配置接口时转 base64 预览
                    const reader = new FileReader()
                    reader.onload = (ev) => {
                      editor.insertContent(`<img src="${ev.target.result}" alt="${file.name}" style="max-width:100%;" />&nbsp;`)
                    }
                    reader.readAsDataURL(file)
                  }
                }
                input.click()
              }
            })
            editor.on('init', () => {
              this._editor = editor
            })
          },
          branding: false,
          promotion: false,
          statusbar: true,
          resize: true,
        }
      }
    },
    computed: {
      editorContent: {
        get() { return this.value },
        set(val) {
          if (this.maxLength > 0) {
            const text = val.replace(/<[^>]*>/g, '')
            if (text.length > this.maxLength) {
              this.$nextTick(() => {
                if (this._editor) {
                  this._editor.setContent(this.value)
                  this._editor.selection.select(this._editor.getBody(), true)
                  this._editor.selection.collapse(false)
                }
              })
              this.msgError(`内容长度不能超过 ${this.maxLength} 个字符`)
              return
            }
          }
          this.$emit('input', val)
        }
      }
    },
    mounted() {
      tinymce.init({})
      document.addEventListener('click', this.closeDropdowns)
    },
    beforeDestroy() {
      document.removeEventListener('click', this.closeDropdowns)
    },
    methods: {
      closeDropdowns(e) {
        if (!this.$el.contains(e.target)) {
          this.presetOpen = false
          this.activeCat = null
        }
      },
      applyPreset(value) {
        this.latexInput = value
        this.presetOpen = false
        this.$nextTick(() => this.renderPreview())
      },
      // 在光标处插入文本（textarea）
      insertAtCursor(text) {
        const el = this.$refs.fmInput
        if (!el) { this.latexInput += text; return }
        const start = el.selectionStart
        const end = el.selectionEnd
        this.latexInput = this.latexInput.slice(0, start) + text + this.latexInput.slice(end)
        this.$nextTick(() => {
          el.focus()
          const pos = start + text.length
          el.setSelectionRange(pos, pos)
          this.renderPreview()
        })
      },
      onInputChange() {
        this.renderPreview()
      },
      renderPreview() {
        this.$nextTick(() => {
          const el = this.$refs.fmPreview
          if (!el) return
          const latex = this.latexInput.trim()
          if (!latex) {
            el.innerHTML = '<span class="fm-preview-empty">公式预览</span>'
            return
          }
          // 每次重置 innerHTML，强制 MathJax 重新解析，避免缓存导致预览不更新
          el.innerHTML = `$$${latex}$$`
          if (!window.MathJax) return
          window.MathJax.typesetClear([el])
          window.MathJax.typesetPromise([el]).catch(() => {})
        })
      },
      typeset(editor) {
        try {
          const body = editor.getBody()
          if (window.MathJax && window.MathJax.typesetPromise && body) {
            window.MathJax.typesetClear([body])
            window.MathJax.typesetPromise([body]).catch(() => {})
          }
        } catch (e) {}
      },
      insertLatex() {
        if (!this.latexInput.trim()) return
        const editor = this._editor
        if (!editor) return
        const latex = this.latexInput.trim()
        this.latexDialogVisible = false
        // 用主页面已加载的 MathJax 3 将 LaTeX 转成 SVG，以 <img> 插入编辑区
        // 避免 iframe 内独立加载 MathJax 的时序问题
        const doInsert = () => {
          const mj = window.MathJax
          if (mj && mj.tex2svgPromise) {
            mj.tex2svgPromise(latex, { display: true }).then(node => {
              const svg = node.querySelector('svg')
              if (!svg) { this._insertLatexFallback(editor, latex); return }
              // 给 SVG 加固定宽高，避免尺寸为 0
              if (!svg.getAttribute('width') || svg.getAttribute('width') === '0') {
                svg.setAttribute('width', '200')
              }
              svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
              const svgStr = svg.outerHTML
              const dataUri = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)))
              editor.insertContent(
                `<img class="math-formula-img" src="${dataUri}" data-latex="${encodeURIComponent(latex)}" alt="${latex}" style="vertical-align:middle;max-height:3em;display:inline-block;" />&nbsp;`
              )
            }).catch(() => this._insertLatexFallback(editor, latex))
          } else if (mj && mj.startup && mj.startup.promise) {
            // MathJax 还未完成初始化，等待后重试
            mj.startup.promise.then(() => this.insertLatex())
          } else {
            this._insertLatexFallback(editor, latex)
          }
        }
        doInsert()
      },
      _insertLatexFallback(editor, latex) {
        // MathJax 不可用时退回文本形式
        editor.insertContent(
          `<span class="math-formula" contenteditable="false" data-latex="${encodeURIComponent(latex)}">\\(${latex}\\)</span>&nbsp;`
        )
      },
      _typesetWhenReady(editor, attempts) {
        attempts = attempts || 0
        if (attempts > 40) return
        try {
          const iframeWin = this._iframeWin || editor.getWin()
          if (iframeWin.MathJax && iframeWin.MathJax.typesetPromise) {
            iframeWin.MathJax.typesetPromise([editor.getBody()]).catch(() => {})
          } else {
            setTimeout(() => this._typesetWhenReady(editor, attempts + 1), 100)
          }
        } catch (e) {}
      }
    }
  }
</script>

<style scoped>
  .tinymce-container {
    width: 100%;
    line-height: normal;
    position: relative;
  }

  /* ===== 弹窗遮罩 ===== */
  .fm-mask {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.5);
    z-index: 99999;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .fm-dialog {
    background: #fff;
    border-radius: 6px;
    box-shadow: 0 6px 30px rgba(0,0,0,0.2);
    width: 1212px;
    max-width: 96vw;
    display: flex;
    flex-direction: column;
    max-height: 90vh;
  }

  /* ===== 标题栏 ===== */
  .fm-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 18px;
    font-size: 16px;
    font-weight: 600;
    color: #333;
    border-bottom: 1px solid #e4e4e4;
    background: #f7f7f7;
    border-radius: 6px 6px 0 0;
  }
  .fm-close {
    cursor: pointer;
    font-size: 22px;
    color: #aaa;
    line-height: 1;
    padding: 0 2px;
  }
  .fm-close:hover { color: #333; }

  /* ===== 工具栏 ===== */
  .fm-toolbar {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 3px;
    padding: 6px 10px;
    border-bottom: 1px solid #e4e4e4;
    background: #fafafa;
  }
  .fm-divider {
    width: 1px;
    height: 28px;
    background: #ddd;
    margin: 0 4px;
    flex-shrink: 0;
  }

  /* 预设 */
  .fm-preset-wrap { position: relative; }
  .fm-preset-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 0 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    background: #e8f4e8;
    cursor: pointer;
    min-width: 48px;
    height: 60px;
    box-sizing: border-box;
    user-select: none;
  }
  .fm-preset-btn:hover { background: #d0ebd0; }
  .fm-preset-icon { font-size: 14px; font-weight: bold; color: #2a7a2a; line-height: 1.2; }
  .fm-preset-label { font-size: 11px; color: #2a7a2a; }
  .fm-preset-arrow { font-size: 10px; color: #2a7a2a; }
  .fm-preset-dropdown {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.12);
    z-index: 10;
    min-width: 160px;
  }
  .fm-preset-item {
    padding: 7px 14px;
    font-size: 13px;
    cursor: pointer;
    color: #333;
    white-space: nowrap;
  }
  .fm-preset-item:hover { background: #f0f7ff; color: #0183FB; }

  /* 符号格子 */
  .fm-symbol-grid {
    display: flex;
    flex-wrap: nowrap;
    gap: 2px;
    flex-shrink: 0;
  }
  .fm-sym-btn {
    width: 40px;
    height: 60px;
    border: 1px solid #ddd;
    background: #fff;
    border-radius: 3px;
    cursor: pointer;
    font-size: 13px;
    color: #333;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
  }
  .fm-sym-btn:hover { background: #e8f0fe; border-color: #0183FB; color: #0183FB; }

  /* 分类按钮组 */
  .fm-category-row {
    display: flex;
    gap: 3px;
    flex-wrap: nowrap;
    flex-shrink: 0;
  }
  .fm-cat-wrap { position: relative; }
  .fm-cat-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 0 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
    background: #fff;
    cursor: pointer;
    min-width: 44px;
    height: 60px;
    box-sizing: border-box;
    user-select: none;
    gap: 1px;
  }
  .fm-cat-btn:hover, .fm-cat-btn.active { background: #e8f0fe; border-color: #0183FB; }
  .fm-cat-icon { font-size: 14px; color: #333; line-height: 1; }
  .fm-cat-label { font-size: 11px; color: #555; }
  .fm-cat-arrow { font-size: 9px; color: #888; }
  .fm-cat-dropdown {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    background: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.12);
    z-index: 10;
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 8px;
    min-width: 160px;
  }
  .fm-cat-item {
    padding: 5px 10px;
    border: 1px solid #ddd;
    border-radius: 3px;
    background: #fff;
    cursor: pointer;
    font-size: 13px;
    color: #333;
    white-space: nowrap;
  }
  .fm-cat-item:hover { background: #e8f0fe; border-color: #0183FB; color: #0183FB; }

  /* ===== 输入区 ===== */
  .fm-body {
    flex: 1;
    display: flex;
    gap: 0;
    min-height: 200px;
    overflow: hidden;
  }
  .fm-input {
    flex: 1;
    border: none;
    border-right: 1px solid #e4e4e4;
    outline: none;
    padding: 20px;
    font-size: 16px;
    font-family: 'Courier New', monospace;
    resize: none;
    color: #333;
    background: #fff;
    line-height: 1.6;
  }
  .fm-input::placeholder {
    color: #bbb;
    font-family: inherit;
  }
  .fm-preview {
    flex: 1;
    padding: 20px;
    background: #fafafa;
    font-size: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 120px;
    overflow: auto;
    color: #333;
  }
  .fm-preview-empty { color: #ccc; font-size: 14px; font-family: sans-serif; }

  /* ===== 底部 ===== */
  .fm-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    padding: 12px 18px;
    border-top: 1px solid #e4e4e4;
    background: #f7f7f7;
    border-radius: 0 0 6px 6px;
  }
  .fm-btn-close, .fm-btn-save {
    padding: 7px 28px;
    border-radius: 4px;
    font-size: 14px;
    cursor: pointer;
    border: 1px solid #ddd;
    outline: none;
  }
  .fm-btn-close { background: #fff; color: #555; }
  .fm-btn-close:hover { background: #f0f0f0; }
  .fm-btn-save { background: #0183FB; border-color: #0183FB; color: #fff; }
  .fm-btn-save:hover { background: #0072e0; }
</style>
