浏览代码

公式组件与描述组件合并

dusenyao 4 月之前
父节点
当前提交
94656f51e4

+ 198 - 0
src/components/MathDialog.vue

@@ -0,0 +1,198 @@
+<template>
+  <el-dialog
+    title="输入公式"
+    :visible="visible"
+    width="850px"
+    top="20vh"
+    :show-close="false"
+    :close-on-press-escape="false"
+    :close-on-click-modal="false"
+    @close="dialogClose"
+  >
+    <el-input
+      v-model="math"
+      type="textarea"
+      :autosize="{ minRows: 4, maxRows: 8 }"
+      resize="none"
+      placeholder="请输入公式"
+      @input="renderMathDialog"
+    />
+
+    <el-tabs v-model="activeTab" type="card">
+      <el-tab-pane
+        v-for="(macros, category) in mathMacrosListByCategory"
+        :key="category"
+        :label="categoryList[category]"
+      >
+        <div class="math-macros">
+          <span
+            v-for="macro in macros"
+            :key="macro"
+            :ref="macro"
+            :class="['macros-item', `${macro}`]"
+            @click="insertMacro(macro)"
+          >
+            {{ computedMacrosText(macro) }}
+          </span>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+
+    <div ref="mathContainer" class="formula-render" v-html="math"></div>
+
+    <template slot="footer">
+      <el-button size="medium" @click="dialogClose">取消</el-button>
+      <el-button type="primary" size="medium" @click="mathConfirm">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+import {
+  mathMacrosListByCategory,
+  onlyMultiMacros,
+  onlyAlignMacros,
+  commonMacros,
+  categoryList,
+} from '@/views/book/courseware/data/describe';
+
+export default {
+  name: 'MathDialog',
+  props: {
+    visible: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      math: '',
+      activeTab: '0', // 用于控制选中的标签页
+      mathMacrosListByCategory,
+      categoryList,
+      openNumber: 0, // 用于控制 MathJax 渲染
+    };
+  },
+  watch: {
+    visible(newVal) {
+      if (newVal) {
+        this.openNumber += 1;
+        this.$nextTick(() => {
+          const macrosRefs = Object.values(mathMacrosListByCategory)
+            .flat()
+            .map((macro) => this.$refs[macro]);
+          window.MathJax.typesetPromise(macrosRefs).catch((err) => {
+            console.error('MathJax 初始化失败:', err);
+          });
+        });
+      } else {
+        this.math = '';
+        this.activeTab = '0';
+      }
+    },
+  },
+  methods: {
+    dialogClose() {
+      this.$emit('update:visible', false);
+      this.$emit('close');
+    },
+    mathConfirm() {
+      this.$emit('confirm', this.math);
+      this.dialogClose();
+    },
+    computedMacrosText(macro) {
+      if (onlyMultiMacros.includes(macro.split(' ')[0])) {
+        return `\\begin{multline}\\${macro}\\end{multline}`;
+      }
+      if (onlyAlignMacros.includes(macro.split('{')[0])) {
+        return `\\begin{align}\\${macro}\\end{align}`;
+      }
+      if (commonMacros.includes(macro)) {
+        return macro;
+      }
+      return `$\\${macro}$`;
+    },
+    /**
+     * 插入宏到公式中
+     * @param {string} macro - 宏字符串
+     */
+    insertMacro(macro) {
+      let _macro = macro.trim();
+      let math = this.math || '';
+      // 去除首尾空白
+      math = math.trim();
+      // 如果宏是多行公式
+      if (onlyMultiMacros.includes(_macro.split(' ')[0])) {
+        _macro = `begin{multline}\\${_macro}\\end{multline}`;
+      }
+      if (onlyAlignMacros.includes(_macro.split('{')[0])) {
+        _macro = `begin{align}\\${_macro}\\end{align}`;
+      }
+      // 判断头部和尾部是否有$
+      let hasHead = math.startsWith('$');
+      let hasTail = math.endsWith('$');
+      if (!hasHead) math = `$${math}`;
+      if (!hasTail) math += '$';
+
+      // 将宏插入到末尾$前
+      if (commonMacros.includes(_macro)) {
+        math = math.replace(/\$$/, ` ${_macro}$`);
+      } else {
+        math = math.replace(/\$$/, ` \\${_macro}$`);
+      }
+      this.math = math;
+      this.renderMathDialog();
+    },
+    async renderMathDialog() {
+      this.dialogMathValidate = false;
+      await this.$nextTick();
+      try {
+        await window.MathJax.typesetPromise([this.$refs.mathContainer]);
+        let mathRes = this.$refs.mathContainer.innerHTML;
+        if (mathRes && mathRes.indexOf('merror') === -1) this.dialogMathValidate = true;
+      } catch (err) {
+        console.error('公式渲染失败:', err);
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.el-dialog {
+  :deep &__body {
+    display: flex;
+    flex-direction: column;
+    row-gap: 12px;
+
+    .math-macros {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px 12px;
+      align-items: center;
+      max-height: 300px;
+      overflow: auto;
+
+      .macros-item {
+        padding: 4px;
+        cursor: pointer;
+        border: $border;
+        border-radius: 4px;
+
+        :deep .MathJax {
+          font-size: 24px !important;
+        }
+      }
+    }
+
+    .formula-render {
+      min-height: 50px;
+      padding: 15px;
+      margin-top: 8px;
+      background: #f8f9fa;
+      border: 1px solid #e9ecef;
+      border-radius: 4px;
+    }
+  }
+}
+</style>

+ 5 - 58
src/components/RichText.vue

@@ -19,35 +19,14 @@
       <span class="button" @click="deleteFill">删除填空</span>
     </div>
 
-    <el-dialog
-      title="输入公式"
-      :visible.sync="isViewMathDialog"
-      width="400px"
-      top="20vh"
-      :show-close="false"
-      :close-on-press-escape="false"
-      :close-on-click-modal="false"
-      @close="isViewMathDialog = false"
-    >
-      <el-input
-        v-model="mathData.math"
-        type="textarea"
-        :autosize="{ minRows: 4, maxRows: 8 }"
-        resize="none"
-        placeholder="请输入公式"
-      />
-      <div ref="mathContainer" v-data-h="mathData.math" class="formula-render" v-html="mathData.math"></div>
-      <template slot="footer">
-        <el-button size="medium" @click="isViewMathDialog = false">取消</el-button>
-        <el-button type="primary" size="medium" @click="mathConfirm">确定</el-button>
-      </template>
-    </el-dialog>
+    <MathDialog :visible.sync="isViewMathDialog" @confirm="mathConfirm" />
   </div>
 </template>
 
 <script>
 import tinymce from 'tinymce/tinymce';
 import Editor from '@tinymce/tinymce-vue';
+import MathDialog from '@/components/MathDialog.vue';
 
 import 'tinymce/icons/default/icons';
 import 'tinymce/themes/silver';
@@ -74,12 +53,12 @@ import { getRandomNumber } from '@/utils';
 import { isNodeType } from '@/utils/validate';
 import { fileUpload } from '@/api/app';
 import { addTone, handleToneValue } from '@/utils/common';
-import { getMathData } from '@/views/book/courseware/data/math';
 
 export default {
   name: 'RichText',
   components: {
     Editor,
+    MathDialog,
   },
   inheritAttrs: false,
   props: {
@@ -136,9 +115,7 @@ export default {
   },
   data() {
     return {
-      mathData: getMathData(),
       isViewMathDialog: false,
-      dialogMathValidate: false,
       mathEleIsInit: true,
       math: '',
       isShow: false,
@@ -233,21 +210,6 @@ export default {
       },
     };
   },
-  watch: {
-    // 监听数据变化,重新渲染 MathJax
-    'mathData.math': {
-      handler(n, o) {
-        if (n) this.renderMathDialog();
-      },
-      deep: true,
-    },
-    isViewMathDialog: {
-      handler() {
-        this.dialogMathValidate = false;
-        this.mathData.math = '';
-      },
-    },
-  },
   created() {
     if (this.pageFrom !== 'audit') {
       window.addEventListener('click', this.hideToolbarDrawer);
@@ -593,18 +555,14 @@ export default {
       };
     },
 
-    mathConfirm() {
-      if (!this.dialogMathValidate) return;
-
+    mathConfirm(math) {
       let editor = tinymce.get(this.id);
       let tmpId = getRandomNumber();
-      let tmpMathData = this.mathData.math;
       editor.insertContent(`
          <span id="${tmpId}" contenteditable="false" class="mathjax-container editor-math">
-           ${tmpMathData}
+           ${math}
          </span>
        `);
-      // editor.insertContent(`${latex}`);
       this.mathEleIsInit = false;
       this.renderMath(tmpId);
       this.isViewMathDialog = false;
@@ -629,17 +587,6 @@ export default {
         this.mathEleIsInit = true;
       }
     },
-    async renderMathDialog() {
-      this.dialogMathValidate = false;
-      await this.$nextTick();
-      try {
-        await window.MathJax.typesetPromise([this.$refs.mathContainer]);
-        let mathRes = this.$refs.mathContainer.innerHTML;
-        if (mathRes && mathRes.indexOf('merror') === -1) this.dialogMathValidate = true;
-      } catch (err) {
-        console.error('公式渲染失败:', err);
-      }
-    },
   },
 };
 </script>

+ 2 - 2
src/views/book/courseware/create/components/base/describe/Describe.vue

@@ -25,14 +25,14 @@
 import { getDescribeData } from '@/views/book/courseware/data/describe';
 import { CrateParsedTextInfo_Pinyin } from '@/api/book';
 import { isEnable } from '@/views/book/courseware/data/common';
+
 import ModuleMixin from '../../common/ModuleMixin';
-import RichText from '@/components/RichText.vue';
 import PinyinText from '@/components/PinyinText.vue';
 import DOMPurify from 'dompurify';
 
 export default {
   name: 'DescribePage',
-  components: { RichText, PinyinText },
+  components: { PinyinText },
   mixins: [ModuleMixin],
   data() {
     return {

+ 26 - 26
src/views/book/courseware/data/bookType.js

@@ -1,8 +1,8 @@
 // 基础、题型组件编辑和设置页面
 import DividerPage from '../create/components/base/divider/Divider.vue';
 import DividerSetting from '../create/components/base/divider/DividerSetting.vue';
-import SpacingPage from '../create/components/base/spacing/Spacing.vue';
-import SpacingSetting from '../create/components/base/spacing/SpacingSetting.vue';
+// import SpacingPage from '../create/components/base/spacing/Spacing.vue';
+// import SpacingSetting from '../create/components/base/spacing/SpacingSetting.vue';
 import AudioPage from '../create/components/base/audio/Audio.vue';
 import AudioSetting from '../create/components/base/audio/AudioSetting.vue';
 import PicturePage from '../create/components/base/picture/Picture.vue';
@@ -27,14 +27,14 @@ import FillPage from '../create/components/question/fill/Fill.vue';
 import FillSetting from '../create/components/question/fill/FillSetting.vue';
 import RecordInput from '../create/components/question/record_input/RecordInput.vue';
 import RecordInputSetting from '../create/components/question/record_input/RecordInputSetting.vue';
-import UploadControl from '../create/components/base/upload_control/UploadControl.vue';
-import UploadControlSetting from '../create/components/base/upload_control/UploadControlSetting.vue';
+// import UploadControl from '../create/components/base/upload_control/UploadControl.vue';
+// import UploadControlSetting from '../create/components/base/upload_control/UploadControlSetting.vue';
 import UploadPreview from '../create/components/base/upload_preview/UploadPreview.vue';
 import UploadRreviewSetting from '../create/components/base/upload_preview/UploadRreviewSetting.vue';
 import PinyinBase from '../create/components/question/pinyin_base/PinyinBase.vue';
 import PinyinBaseSetting from '../create/components/question/pinyin_base/PinyinBaseSetting.vue';
-import CharacterBase from '../create/components/base/character_base/CharacterBase.vue';
-import CharacterBaseSetting from '../create/components/base/character_base/CharacterBaseSetting.vue';
+// import CharacterBase from '../create/components/base/character_base/CharacterBase.vue';
+// import CharacterBaseSetting from '../create/components/base/character_base/CharacterBaseSetting.vue';
 import Character from '../create/components/question/character/Character.vue';
 import CharacterSetting from '../create/components/question/character/CharacterSetting.vue';
 import Write from '../create/components/question/write/Write.vue';
@@ -43,12 +43,12 @@ import NewWord from '../create/components/question/new_word/NewWord.vue';
 import NewWordSetting from '../create/components/question/new_word/NewWordSetting.vue';
 import Notes from '../create/components/question/notes/Notes.vue';
 import NotesSetting from '../create/components/question/notes/NotesSetting.vue';
-import OtherWord from '../create/components/question/other_word/OtherWord.vue';
-import OtherWordSetting from '../create/components/question/other_word/OtherWordSetting.vue';
-import Article from '../create/components/question/article/Article.vue';
-import ArticleSetting from '../create/components/question/article/ArticleSetting.vue';
-import Math from '../create/components/question/math/Math.vue';
-import MathSetting from '../create/components/question/math/MathSetting.vue';
+// import OtherWord from '../create/components/question/other_word/OtherWord.vue';
+// import OtherWordSetting from '../create/components/question/other_word/OtherWordSetting.vue';
+// import Article from '../create/components/question/article/Article.vue';
+// import ArticleSetting from '../create/components/question/article/ArticleSetting.vue';
+// import Math from '../create/components/question/math/Math.vue';
+// import MathSetting from '../create/components/question/math/MathSetting.vue';
 import Input from '../create/components/question/input/Input.vue';
 import InputSetting from '../create/components/question/input/InputSetting.vue';
 import Judge from '../create/components/question/judge/Judge.vue';
@@ -69,7 +69,7 @@ import ThreeModelSetting from '../create/components/base/3d_model/3DModelSetting
 // 预览组件页面列表
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
 import DividerPreview from '@/views/book/courseware/preview/components/divider/DividerPreview.vue';
-import SpacingPreview from '@/views/book/courseware/preview/components/spacing/SpacingPreview.vue';
+// import SpacingPreview from '@/views/book/courseware/preview/components/spacing/SpacingPreview.vue';
 import PicturePreview from '@/views/book/courseware/preview/components/picture/PicturePreview.vue';
 import VideoPreview from '@/views/book/courseware/preview/components/video/VideoPreview.vue';
 import StemPreview from '@/views/book/courseware/preview/components/stem/StemPreview.vue';
@@ -81,17 +81,17 @@ import SortPreview from '@/views/book/courseware/preview/components/sort/SortPre
 import VoiceMatrixPreview from '@/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue';
 import FillPreview from '@/views/book/courseware/preview/components/fill/FillPreview.vue';
 import RecordInputPreview from '../preview/components/record_input/RecordInputPreview.vue';
-import UploadControlPreview from '../preview/components/upload_control/UploadControlPreview.vue';
+// import UploadControlPreview from '../preview/components/upload_control/UploadControlPreview.vue';
 import UploadPreviewPreview from '../preview/components/upload_preview/UploadPreviewPreview.vue';
 import PinyinBasePreview from '../preview/components/pinyin_base/PinyinBasePreview.vue';
-import CharacterBasePreview from '../preview/components/character_base/CharacterBasePreview.vue';
+// import CharacterBasePreview from '../preview/components/character_base/CharacterBasePreview.vue';
 import CharacterPreview from '../preview/components/character/CharacterPreview.vue';
 import WritePreview from '../preview/components/write/WritePreview.vue';
 import NewWordPreview from '../preview/components/new_word/NewWordPreview.vue';
 import NotesPreview from '../preview/components/notes/NotesPreview.vue';
-import OtherWordPreview from '../preview/components/other_word/OtherWordPreview.vue';
-import ArticlePreview from '../preview/components/article/index.vue';
-import MathPreview from '../preview/components/math/MathPreview.vue';
+// import OtherWordPreview from '../preview/components/other_word/OtherWordPreview.vue';
+// import ArticlePreview from '../preview/components/article/index.vue';
+// import MathPreview from '../preview/components/math/MathPreview.vue';
 import InputPreview from '../preview/components/input/InputPreview.vue';
 
 import JudgePreview from '../preview/components/judge/JudgePreview.vue';
@@ -322,14 +322,14 @@ export const bookTypeOption = [
         set: WriteSetting,
         preview: WritePreview,
       },
-      {
-        value: 'math',
-        label: '公式组件',
-        icon: '',
-        component: Math,
-        set: MathSetting,
-        preview: MathPreview,
-      },
+      // {
+      //   value: 'math',
+      //   label: '公式组件',
+      //   icon: '',
+      //   component: Math,
+      //   set: MathSetting,
+      //   preview: MathPreview,
+      // },
       {
         value: 'input',
         label: '输入框组件',

+ 440 - 0
src/views/book/courseware/data/describe.js

@@ -6,6 +6,446 @@ import {
   pinyinPositionList,
 } from '@/views/book/courseware/data/common';
 
+// 按用途分类的数学公式宏列表
+export const mathMacrosListByCategory = {
+  common: [
+    'frac{1}{2}',
+    'sqrt{2}',
+    'sum',
+    'neq',
+    'geqslant',
+    'leqslant',
+    'abs{a}',
+    'cap',
+    'cup',
+    'acomm{A}{B}',
+    'acos(x)',
+    'comm{A}{B}',
+    'ip{1}',
+    'in',
+    'ni',
+    'subset',
+    'supset',
+    'subseteq',
+    'supseteq',
+    'leftarrow',
+    'rightarrow',
+    '+',
+    '-',
+    '*',
+    'divsymbol',
+    '/',
+    'pi',
+    'alpha',
+    'beta',
+    'infty',
+    'emptyset',
+  ],
+  // 关系符号
+  relation: [
+    'approxeq',
+    'backsimeq',
+    'bumpeq',
+    'Bumpeq',
+    'circeq',
+    'doteqdot',
+    'Doteq',
+    'eqcirc',
+    'eqsim',
+    'eqslantgtr',
+    'eqslantless',
+    'fallingdotseq',
+    'geqq',
+    'ggg',
+    'gggtr',
+    'gnapprox',
+    'gnsim',
+    'gtrapprox',
+    'gtrdot',
+    'gtreqless',
+    'gtreqqless',
+    'gtrless',
+    'gtrsim',
+    'leqq',
+    'lessapprox',
+    'lessdot',
+    'lesseqgtr',
+    'lesseqqgtr',
+    'lessgtr',
+    'lesssim',
+    'lll',
+    'llless',
+    'lnapprox',
+    'lneqq',
+    'lnsim',
+    'ncong',
+    'nless',
+    'nprec',
+    'npreceq',
+    'nsim',
+    'nsucc',
+    'nsucceq',
+    'ntriangleleft',
+    'ntrianglelefteq',
+    'ntriangleright',
+    'ntrianglerighteq',
+    'prec',
+    'precapprox',
+    'preccurlyeq',
+    'precnapprox',
+    'precneqq',
+    'precnsim',
+    'precsim',
+    'succ',
+    'succapprox',
+    'succcurlyeq',
+    'succnapprox',
+    'succneqq',
+    'succnsim',
+    'succsim',
+    'Subset',
+    'subseteqq',
+    'Supset',
+    'supseteqq',
+    'sim',
+    'simeq',
+    'thickapprox',
+    'thicksim',
+    'trianglelefteq',
+    'trianglerighteq',
+    'triangleq',
+    'vartriangle',
+    'vartriangleleft',
+    'vartriangleright',
+    'vdash',
+    'vDash',
+    'Vdash',
+    'Vvdash',
+    'models',
+    'nvdash',
+    'shortmid',
+    'shortparallel',
+    'mid',
+    'parallel',
+    'dashv',
+    'nvDash',
+    'nVdash',
+    'nVDash',
+    'between',
+    'because',
+    'therefore',
+    'Join',
+    'leadsto',
+    'pitchfork',
+    'propto',
+    'varpropto',
+    'restriction',
+    'smallfrown',
+    'smallsmile',
+    'smile',
+    'frown',
+    'asymp',
+    'approx',
+    'cong',
+    'equiv',
+    'ne',
+    'notag',
+    'preceq',
+    'succeq',
+  ],
+  // 箭头符号
+  arrows: [
+    'backepsilon',
+    'curvearrowleft',
+    'curvearrowright',
+    'dashleftarrow',
+    'dashrightarrow',
+    'downdownarrows',
+    'downharpoonleft',
+    'downharpoonright',
+    'hookleftarrow',
+    'hookrightarrow',
+    'Leftarrow',
+    'leftarrowtail',
+    'leftleftarrows',
+    'leftrightarrow',
+    'Leftrightarrow',
+    'leftrightarrows',
+    'leftrightharpoons',
+    'leftrightsquigarrow',
+    'looparrowleft',
+    'looparrowright',
+    'Lsh',
+    'Rightarrow',
+    'rightarrowtail',
+    'rightleftarrows',
+    'rightleftharpoons',
+    'rightrightarrows',
+    'rightsquigarrow',
+    'Rrightarrow',
+    'Rsh',
+    'twoheadleftarrow',
+    'twoheadrightarrow',
+    'upharpoonleft',
+    'upharpoonright',
+    'upuparrows',
+    'implies',
+    'impliedby',
+    'leadsto',
+    'mapsto',
+    'nleftarrow',
+    'nLeftarrow',
+    'nleftrightarrow',
+    'nLeftrightarrow',
+    'nrightarrow',
+    'nRightarrow',
+    'xleftarrow{1}',
+    'xrightarrow{1}',
+  ],
+  // 运算符号
+  operators: [
+    'barwedge',
+    'bigstar',
+    'binom{1}{2}',
+    'boxdot',
+    'boxminus',
+    'boxplus',
+    'boxtimes',
+    'Cap',
+    'centerdot',
+    'cfrac{1}{2}',
+    'checkmark',
+    'circledast',
+    'circledcirc',
+    'circleddash',
+    'complement',
+    'Cup',
+    'curlyvee',
+    'curlywedge',
+    'dbinom{1}{2}',
+    'ddddot{x}',
+    'dddot{x}',
+    'dfrac{a}{b}',
+    'Diamond',
+    'divideontimes',
+    'dotplus',
+    'doublebarwedge',
+    'doublecap',
+    'doublecup',
+    'genfrac{}{}{}{}{1}{4}',
+    'idotsint',
+    'iiiint',
+    'int',
+    'iint',
+    'iiint',
+    'intercal',
+    'land',
+    'lor',
+    'maltese',
+    'measuredangle',
+    'mho',
+    'multimap',
+    'prod',
+    'projlim',
+    'rtimes',
+    'ltimes',
+    'sideset{^*}{_a}\\sum',
+    'smallsetminus',
+    'sqcap',
+    'sqcup',
+    'star',
+    'substack{a \\\\ b}',
+    'tbinom{1}{2}',
+    'tfrac{1}{2}',
+    'varinjlim',
+    'varliminf',
+    'varlimsup',
+    'varprojlim',
+    'wedge',
+    'vee',
+    'veebar',
+    'wr',
+  ],
+  // 集合符号
+  sets: [
+    'beth',
+    'daleth',
+    'Finv',
+    'Game',
+    'gimel',
+    'varnothing',
+    'notin',
+    'sqsubset',
+    'sqsupset',
+    'sqsubseteq',
+    'sqsupseteq',
+  ],
+  // 物理符号
+  physics: [
+    'acosine',
+    'acot(x)',
+    'acsc(x)',
+    'admat{1, 2, 3}',
+    'anticommutator{A}{B}',
+    'arccos(x)',
+    'arccot(x)',
+    'arccsc(x)',
+    'arcsec(x)',
+    'arcsin(x)',
+    'arctan(x)',
+    'asec(x)',
+    'asin(x)',
+    'atan(x)',
+    'bmqty{x}',
+    'bqty{x}',
+    'Bqty{x}',
+    'bra{\\phi}',
+    'braket{a}{b}',
+    'cos{1}',
+    'cosecant{1}',
+    'cosh{1}',
+    'cot{1}',
+    'coth{1}',
+    'cross{1}',
+    'csc{1}',
+    'csch{1}',
+    'curl{1}',
+    'dd{1}',
+    'det{1}',
+    'diagonalmatrix{1}',
+    'diffd{1}',
+    'div{1}',
+    'dv{1}',
+    'dyad{1}',
+    'erf{1}',
+    'ev{1}',
+    'eval{1}',
+    'exp{1}',
+    'expval{1}',
+    'fdv{1}',
+    'flatfrac{1}{2}',
+    'grad{1}',
+    'hypcosecant{1}',
+    'hypcosine{1}',
+    'hypcotangent{1}',
+    'hypsecant{1}',
+    'hypsine{1}',
+    'hyptangent{1}',
+    'Im{1}',
+    'imat{1}',
+    'ket{1}',
+    'ketbra{1}',
+    'laplacian{1}',
+    'ln{1}',
+    'log{1}',
+    'mdet{a & b \\newline c & d}',
+    'mel{n}{A}{m}',
+    'mqty{a & b \\newline c & d}',
+    'naturallogarithm{1}',
+    'norm{a}',
+    'op{1}',
+    'order{1}',
+    'pb{A}{B}',
+    'pdv{f}{x}',
+    'pmat{n}',
+    'pmqty{1}',
+    'Pmqty{1}',
+    'pqty{1}',
+    'Pr(tall)',
+    'pv{\\int f(z) \\dd{z}}',
+    'PV{\\int f(z) \\dd{z}}',
+    'qty{3.0}{m/s}',
+    'Re{z}',
+    'Res[f(z)]',
+    'sbmqty{1}',
+    'sec{x}',
+    'sech{1}',
+    'sin{1}',
+    'sinh{1}',
+    'smdet{1}',
+    'smqty{a & b \\newline c & d}',
+    'spmqty{a & b \\newline c & d}',
+    'sPmqty{a & b \\newline c & d}',
+    'svmqty{a & b \\newline c & d}',
+    'tan{1}',
+    'tanh{1}',
+    'tr{1}',
+    'Tr{1}',
+    'va{1}',
+    'var{F[g(x)]}',
+    'vb{a}',
+    'vdot',
+    'vmqty{x}',
+    'vnabla',
+    'vqty{x}',
+    'vu{a}',
+    'xmat{1}{2}{3}',
+    'zmat{2}{2}',
+  ],
+  // 希腊字母/变体
+  greek: [
+    'varDelta',
+    'varGamma',
+    'varLambda',
+    'varOmega',
+    'varPhi',
+    'varPi',
+    'varPsi',
+    'varSigma',
+    'varTheta',
+    'varUpsilon',
+    'varXi',
+  ],
+  // 括号/分隔符
+  brackets: ['lvert', 'lVert', 'rvert', 'rVert', 'llcorner', 'lrcorner', 'ulcorner', 'urcorner'],
+  // 其他(文本、空格、特殊符号等)
+  others: [
+    'Bbbk',
+    'blacklozenge',
+    'blacksquare',
+    'blacktriangle',
+    'blacktriangledown',
+    'blacktriangleleft',
+    'blacktriangleright',
+    'Box',
+    'boxed{Cup}',
+    'diagdown',
+    'diagup',
+    'eqref{eq1}',
+    'eth',
+    'hslash',
+    'mathring{x}',
+    'negmedspace',
+    'negthickspace',
+    'nexists',
+    'nobreakspace',
+    'operatorname{foo}',
+    'shoveleft x + y = z \\newline a + b = c',
+    'shoveright  x + y = z \\newline a + b = c',
+    'sphericalangle',
+    'tag{1}',
+    'yen',
+  ],
+};
+
+export const onlyMultiMacros = ['shoveleft', 'shoveright'];
+
+export const onlyAlignMacros = ['admat', 'xmat', 'zmat'];
+
+export const commonMacros = ['+', '-', '*', '/'];
+
+export const categoryList = {
+  common: '常用',
+  operators: '运算符号',
+  relation: '关系符号',
+  arrows: '箭头符号',
+  sets: '集合符号',
+  physics: '物理符号',
+  greek: '希腊字母',
+  brackets: '分隔符',
+  others: '其他',
+};
+
 export function getDescribeProperty() {
   return {
     serial_number: 1, // 序号

+ 18 - 17
src/views/book/courseware/preview/components/matching/MatchingPreview.vue

@@ -429,24 +429,25 @@ export default {
           }
         }),
       );
-      // 如果当前选项有 preMark 或 nextMark,则清除原来的连线
-      if (!oldPointer.mark) return;
-      document.querySelector(`svg.connection-line.svg-${curMark}-${oldPointer.mark}`)?.remove();
-      document.querySelector(`svg.connection-line.svg-${oldPointer.mark}-${curMark}`)?.remove();
+      // 打开这个,一个只能连一个
+      // // 如果当前选项有 preMark 或 nextMark,则清除原来的连线
+      // if (!oldPointer.mark) return;
+      // document.querySelector(`svg.connection-line.svg-${curMark}-${oldPointer.mark}`)?.remove();
+      // document.querySelector(`svg.connection-line.svg-${oldPointer.mark}-${curMark}`)?.remove();
 
-      // 找到原来的选项,清除 preMark 或 nextMark
-      this.answerList.find((item) =>
-        item.find((li) => {
-          if (li.mark === oldPointer.mark) {
-            if (oldPointer.position === 'pre') {
-              li.nextMark = '';
-            } else {
-              li.preMark = '';
-            }
-            return true;
-          }
-        }),
-      );
+      // // 找到原来的选项,清除 preMark 或 nextMark
+      // this.answerList.find((item) =>
+      //   item.find((li) => {
+      //     if (li.mark === oldPointer.mark) {
+      //       if (oldPointer.position === 'pre') {
+      //         li.nextMark = '';
+      //       } else {
+      //         li.preMark = '';
+      //       }
+      //       return true;
+      //     }
+      //   }),
+      // );
     },
     /**
      * 根据 mark 查找 nextMark 或 preMark