dedsudiyu 1 week ago
parent
commit
a178c06c9b
53 changed files with 19528 additions and 0 deletions
  1. 1 0
      public/mathjax/output-svg/fonts/tex.js
  2. 1 0
      public/mathjax/tex-svg.js
  3. 182 0
      public/tinymce/icons/default/icons.js
  4. 1 0
      public/tinymce/icons/default/icons.min.js
  5. 7 0
      public/tinymce/icons/default/index.js
  6. 1 0
      public/tinymce/langs/zh_CN.js
  7. 1 0
      public/tinymce/plugins/emoticons/js/emojis.js
  8. 2 0
      public/tinymce/plugins/emoticons/js/emojis.min.js
  9. 66 0
      public/tinymce/skins/content/dark/content.css
  10. 1 0
      public/tinymce/skins/content/dark/content.min.css
  11. 61 0
      public/tinymce/skins/content/default/content.css
  12. 1 0
      public/tinymce/skins/content/default/content.min.css
  13. 66 0
      public/tinymce/skins/content/document/content.css
  14. 1 0
      public/tinymce/skins/content/document/content.min.css
  15. 66 0
      public/tinymce/skins/content/tinymce-5-dark/content.css
  16. 1 0
      public/tinymce/skins/content/tinymce-5-dark/content.min.css
  17. 61 0
      public/tinymce/skins/content/tinymce-5/content.css
  18. 1 0
      public/tinymce/skins/content/tinymce-5/content.min.css
  19. 62 0
      public/tinymce/skins/content/writer/content.css
  20. 1 0
      public/tinymce/skins/content/writer/content.min.css
  21. 711 0
      public/tinymce/skins/ui/oxide-dark/content.css
  22. 724 0
      public/tinymce/skins/ui/oxide-dark/content.inline.css
  23. 1 0
      public/tinymce/skins/ui/oxide-dark/content.inline.min.css
  24. 1 0
      public/tinymce/skins/ui/oxide-dark/content.min.css
  25. 3030 0
      public/tinymce/skins/ui/oxide-dark/skin.css
  26. 1 0
      public/tinymce/skins/ui/oxide-dark/skin.min.css
  27. 30 0
      public/tinymce/skins/ui/oxide-dark/skin.shadowdom.css
  28. 1 0
      public/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
  29. 730 0
      public/tinymce/skins/ui/oxide/content.css
  30. 724 0
      public/tinymce/skins/ui/oxide/content.inline.css
  31. 1 0
      public/tinymce/skins/ui/oxide/content.inline.min.css
  32. 1 0
      public/tinymce/skins/ui/oxide/content.min.css
  33. 3027 0
      public/tinymce/skins/ui/oxide/skin.css
  34. 1 0
      public/tinymce/skins/ui/oxide/skin.min.css
  35. 30 0
      public/tinymce/skins/ui/oxide/skin.shadowdom.css
  36. 1 0
      public/tinymce/skins/ui/oxide/skin.shadowdom.min.css
  37. 711 0
      public/tinymce/skins/ui/tinymce-5-dark/content.css
  38. 724 0
      public/tinymce/skins/ui/tinymce-5-dark/content.inline.css
  39. 1 0
      public/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css
  40. 1 0
      public/tinymce/skins/ui/tinymce-5-dark/content.min.css
  41. 3100 0
      public/tinymce/skins/ui/tinymce-5-dark/skin.css
  42. 1 0
      public/tinymce/skins/ui/tinymce-5-dark/skin.min.css
  43. 30 0
      public/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.css
  44. 1 0
      public/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css
  45. 730 0
      public/tinymce/skins/ui/tinymce-5/content.css
  46. 724 0
      public/tinymce/skins/ui/tinymce-5/content.inline.css
  47. 1 0
      public/tinymce/skins/ui/tinymce-5/content.inline.min.css
  48. 1 0
      public/tinymce/skins/ui/tinymce-5/content.min.css
  49. 3100 0
      public/tinymce/skins/ui/tinymce-5/skin.css
  50. 1 0
      public/tinymce/skins/ui/tinymce-5/skin.min.css
  51. 30 0
      public/tinymce/skins/ui/tinymce-5/skin.shadowdom.css
  52. 1 0
      public/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css
  53. 773 0
      src/components/tinymceContainer/index.vue

File diff suppressed because it is too large
+ 1 - 0
public/mathjax/output-svg/fonts/tex.js


File diff suppressed because it is too large
+ 1 - 0
public/mathjax/tex-svg.js


File diff suppressed because it is too large
+ 182 - 0
public/tinymce/icons/default/icons.js


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/icons/default/icons.min.js


+ 7 - 0
public/tinymce/icons/default/index.js

@@ -0,0 +1,7 @@
+// Exports the "default" icons for usage with module loaders
+// Usage:
+//   CommonJS:
+//     require('tinymce/icons/default')
+//   ES2015:
+//     import 'tinymce/icons/default'
+require('./icons.js');

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/langs/zh_CN.js


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/plugins/emoticons/js/emojis.js


File diff suppressed because it is too large
+ 2 - 0
public/tinymce/plugins/emoticons/js/emojis.min.js


+ 66 - 0
public/tinymce/skins/content/dark/content.css

@@ -0,0 +1,66 @@
+body {
+  background-color: #222f3e;
+  color: #fff;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem;
+}
+a {
+  color: #4099ff;
+}
+table {
+  border-collapse: collapse;
+}
+/* Apply a default padding if legacy cellpadding attribute is missing */
+table:not([cellpadding]) th,
+table:not([cellpadding]) td {
+  padding: 0.4rem;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-width"]) th,
+table[border]:not([border="0"]):not([style*="border-width"]) td {
+  border-width: 1px;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-style"]) th,
+table[border]:not([border="0"]):not([style*="border-style"]) td {
+  border-style: solid;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-color"]) th,
+table[border]:not([border="0"]):not([style*="border-color"]) td {
+  border-color: #6d737b;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #8a8f97;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #6d737b;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #6d737b;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #6d737b;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #6d737b;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/content/dark/content.min.css


+ 61 - 0
public/tinymce/skins/content/default/content.css

@@ -0,0 +1,61 @@
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem;
+}
+table {
+  border-collapse: collapse;
+}
+/* Apply a default padding if legacy cellpadding attribute is missing */
+table:not([cellpadding]) th,
+table:not([cellpadding]) td {
+  padding: 0.4rem;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-width"]) th,
+table[border]:not([border="0"]):not([style*="border-width"]) td {
+  border-width: 1px;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-style"]) th,
+table[border]:not([border="0"]):not([style*="border-style"]) td {
+  border-style: solid;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-color"]) th,
+table[border]:not([border="0"]):not([style*="border-color"]) td {
+  border-color: #ccc;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #999;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #ccc;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #e8e8e8;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #ccc;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #ccc;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/content/default/content.min.css


+ 66 - 0
public/tinymce/skins/content/document/content.css

@@ -0,0 +1,66 @@
+@media screen {
+  html {
+    background: #f4f4f4;
+    min-height: 100%;
+  }
+}
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+}
+@media screen {
+  body {
+    background-color: #fff;
+    box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
+    box-sizing: border-box;
+    margin: 1rem auto 0;
+    max-width: 820px;
+    min-height: calc(100vh - 1rem);
+    padding: 4rem 6rem 6rem 6rem;
+  }
+}
+table {
+  border-collapse: collapse;
+}
+/* Apply a default padding if legacy cellpadding attribute is missing */
+table:not([cellpadding]) th,
+table:not([cellpadding]) td {
+  padding: 0.4rem;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-width"]) th,
+table[border]:not([border="0"]):not([style*="border-width"]) td {
+  border-width: 1px;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-style"]) th,
+table[border]:not([border="0"]):not([style*="border-style"]) td {
+  border-style: solid;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-color"]) th,
+table[border]:not([border="0"]):not([style*="border-color"]) td {
+  border-color: #ccc;
+}
+figure figcaption {
+  color: #999;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #ccc;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #ccc;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #ccc;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/content/document/content.min.css


+ 66 - 0
public/tinymce/skins/content/tinymce-5-dark/content.css

@@ -0,0 +1,66 @@
+body {
+  background-color: #2f3742;
+  color: #dfe0e4;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem;
+}
+a {
+  color: #4099ff;
+}
+table {
+  border-collapse: collapse;
+}
+/* Apply a default padding if legacy cellpadding attribute is missing */
+table:not([cellpadding]) th,
+table:not([cellpadding]) td {
+  padding: 0.4rem;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-width"]) th,
+table[border]:not([border="0"]):not([style*="border-width"]) td {
+  border-width: 1px;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-style"]) th,
+table[border]:not([border="0"]):not([style*="border-style"]) td {
+  border-style: solid;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-color"]) th,
+table[border]:not([border="0"]):not([style*="border-color"]) td {
+  border-color: #6d737b;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #8a8f97;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #6d737b;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #6d737b;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #6d737b;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #6d737b;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/content/tinymce-5-dark/content.min.css


+ 61 - 0
public/tinymce/skins/content/tinymce-5/content.css

@@ -0,0 +1,61 @@
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem;
+}
+table {
+  border-collapse: collapse;
+}
+/* Apply a default padding if legacy cellpadding attribute is missing */
+table:not([cellpadding]) th,
+table:not([cellpadding]) td {
+  padding: 0.4rem;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-width"]) th,
+table[border]:not([border="0"]):not([style*="border-width"]) td {
+  border-width: 1px;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-style"]) th,
+table[border]:not([border="0"]):not([style*="border-style"]) td {
+  border-style: solid;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-color"]) th,
+table[border]:not([border="0"]):not([style*="border-color"]) td {
+  border-color: #ccc;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #999;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #ccc;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #e8e8e8;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #ccc;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #ccc;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/content/tinymce-5/content.min.css


+ 62 - 0
public/tinymce/skins/content/writer/content.css

@@ -0,0 +1,62 @@
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  line-height: 1.4;
+  margin: 1rem auto;
+  max-width: 900px;
+}
+table {
+  border-collapse: collapse;
+}
+/* Apply a default padding if legacy cellpadding attribute is missing */
+table:not([cellpadding]) th,
+table:not([cellpadding]) td {
+  padding: 0.4rem;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-width"]) th,
+table[border]:not([border="0"]):not([style*="border-width"]) td {
+  border-width: 1px;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-style"]) th,
+table[border]:not([border="0"]):not([style*="border-style"]) td {
+  border-style: solid;
+}
+/* Set default table styles if a table has a positive border attribute
+   and no inline css */
+table[border]:not([border="0"]):not([style*="border-color"]) th,
+table[border]:not([border="0"]):not([style*="border-color"]) td {
+  border-color: #ccc;
+}
+figure {
+  display: table;
+  margin: 1rem auto;
+}
+figure figcaption {
+  color: #999;
+  display: block;
+  margin-top: 0.25rem;
+  text-align: center;
+}
+hr {
+  border-color: #ccc;
+  border-style: solid;
+  border-width: 1px 0 0 0;
+}
+code {
+  background-color: #e8e8e8;
+  border-radius: 3px;
+  padding: 0.1rem 0.2rem;
+}
+.mce-content-body:not([dir=rtl]) blockquote {
+  border-left: 2px solid #ccc;
+  margin-left: 1.5rem;
+  padding-left: 1rem;
+}
+.mce-content-body[dir=rtl] blockquote {
+  border-right: 2px solid #ccc;
+  margin-right: 1.5rem;
+  padding-right: 1rem;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/content/writer/content.min.css


File diff suppressed because it is too large
+ 711 - 0
public/tinymce/skins/ui/oxide-dark/content.css


File diff suppressed because it is too large
+ 724 - 0
public/tinymce/skins/ui/oxide-dark/content.inline.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide-dark/content.inline.min.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide-dark/content.min.css


File diff suppressed because it is too large
+ 3030 - 0
public/tinymce/skins/ui/oxide-dark/skin.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide-dark/skin.min.css


+ 30 - 0
public/tinymce/skins/ui/oxide-dark/skin.shadowdom.css

@@ -0,0 +1,30 @@
+body.tox-dialog__disable-scroll {
+  overflow: hidden;
+}
+.tox-fullscreen {
+  border: 0;
+  height: 100%;
+  margin: 0;
+  overflow: hidden;
+  overscroll-behavior: none;
+  padding: 0;
+  touch-action: pinch-zoom;
+  width: 100%;
+}
+.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle {
+  display: none;
+}
+.tox.tox-tinymce.tox-fullscreen,
+.tox-shadowhost.tox-fullscreen {
+  left: 0;
+  position: fixed;
+  top: 0;
+  z-index: 1200;
+}
+.tox.tox-tinymce.tox-fullscreen {
+  background-color: transparent;
+}
+.tox-fullscreen .tox.tox-tinymce-aux,
+.tox-fullscreen ~ .tox.tox-tinymce-aux {
+  z-index: 1201;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css


File diff suppressed because it is too large
+ 730 - 0
public/tinymce/skins/ui/oxide/content.css


File diff suppressed because it is too large
+ 724 - 0
public/tinymce/skins/ui/oxide/content.inline.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide/content.inline.min.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide/content.min.css


File diff suppressed because it is too large
+ 3027 - 0
public/tinymce/skins/ui/oxide/skin.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide/skin.min.css


+ 30 - 0
public/tinymce/skins/ui/oxide/skin.shadowdom.css

@@ -0,0 +1,30 @@
+body.tox-dialog__disable-scroll {
+  overflow: hidden;
+}
+.tox-fullscreen {
+  border: 0;
+  height: 100%;
+  margin: 0;
+  overflow: hidden;
+  overscroll-behavior: none;
+  padding: 0;
+  touch-action: pinch-zoom;
+  width: 100%;
+}
+.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle {
+  display: none;
+}
+.tox.tox-tinymce.tox-fullscreen,
+.tox-shadowhost.tox-fullscreen {
+  left: 0;
+  position: fixed;
+  top: 0;
+  z-index: 1200;
+}
+.tox.tox-tinymce.tox-fullscreen {
+  background-color: transparent;
+}
+.tox-fullscreen .tox.tox-tinymce-aux,
+.tox-fullscreen ~ .tox.tox-tinymce-aux {
+  z-index: 1201;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/oxide/skin.shadowdom.min.css


File diff suppressed because it is too large
+ 711 - 0
public/tinymce/skins/ui/tinymce-5-dark/content.css


File diff suppressed because it is too large
+ 724 - 0
public/tinymce/skins/ui/tinymce-5-dark/content.inline.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5-dark/content.min.css


File diff suppressed because it is too large
+ 3100 - 0
public/tinymce/skins/ui/tinymce-5-dark/skin.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5-dark/skin.min.css


+ 30 - 0
public/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.css

@@ -0,0 +1,30 @@
+body.tox-dialog__disable-scroll {
+  overflow: hidden;
+}
+.tox-fullscreen {
+  border: 0;
+  height: 100%;
+  margin: 0;
+  overflow: hidden;
+  overscroll-behavior: none;
+  padding: 0;
+  touch-action: pinch-zoom;
+  width: 100%;
+}
+.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle {
+  display: none;
+}
+.tox.tox-tinymce.tox-fullscreen,
+.tox-shadowhost.tox-fullscreen {
+  left: 0;
+  position: fixed;
+  top: 0;
+  z-index: 1200;
+}
+.tox.tox-tinymce.tox-fullscreen {
+  background-color: transparent;
+}
+.tox-fullscreen .tox.tox-tinymce-aux,
+.tox-fullscreen ~ .tox.tox-tinymce-aux {
+  z-index: 1201;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css


File diff suppressed because it is too large
+ 730 - 0
public/tinymce/skins/ui/tinymce-5/content.css


File diff suppressed because it is too large
+ 724 - 0
public/tinymce/skins/ui/tinymce-5/content.inline.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5/content.inline.min.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5/content.min.css


File diff suppressed because it is too large
+ 3100 - 0
public/tinymce/skins/ui/tinymce-5/skin.css


File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5/skin.min.css


+ 30 - 0
public/tinymce/skins/ui/tinymce-5/skin.shadowdom.css

@@ -0,0 +1,30 @@
+body.tox-dialog__disable-scroll {
+  overflow: hidden;
+}
+.tox-fullscreen {
+  border: 0;
+  height: 100%;
+  margin: 0;
+  overflow: hidden;
+  overscroll-behavior: none;
+  padding: 0;
+  touch-action: pinch-zoom;
+  width: 100%;
+}
+.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle {
+  display: none;
+}
+.tox.tox-tinymce.tox-fullscreen,
+.tox-shadowhost.tox-fullscreen {
+  left: 0;
+  position: fixed;
+  top: 0;
+  z-index: 1200;
+}
+.tox.tox-tinymce.tox-fullscreen {
+  background-color: transparent;
+}
+.tox-fullscreen .tox.tox-tinymce-aux,
+.tox-fullscreen ~ .tox.tox-tinymce-aux {
+  z-index: 1201;
+}

File diff suppressed because it is too large
+ 1 - 0
public/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css


+ 773 - 0
src/components/tinymceContainer/index.vue

@@ -0,0 +1,773 @@
+<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"
+          />
+          <!-- 实时预览 -->
+          <div class="fm-preview" ref="fmPreview">
+            <span v-if="latexInput">$${{ latexInput }}$$</span>
+            <span v-else 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 },
+    // 图片/附件上传接口地址
+    uploadUrl: { type: String, default: '' },
+    // 上传请求头,如 { Authorization: 'Bearer xxx' }
+    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 | latex | 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,
+        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) => {
+          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) { 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 || !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: 720px;
+  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: wrap;
+  gap: 4px;
+  padding: 8px 12px;
+  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;
+  padding: 4px 10px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  background: #e8f4e8;
+  cursor: pointer;
+  min-width: 48px;
+  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: grid;
+  grid-template-columns: repeat(9, 28px);
+  grid-template-rows: repeat(2, 26px);
+  gap: 2px;
+}
+.fm-sym-btn {
+  width: 28px;
+  height: 26px;
+  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: 4px;
+  flex-wrap: wrap;
+}
+.fm-cat-wrap { position: relative; }
+.fm-cat-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 3px 8px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  background: #fff;
+  cursor: pointer;
+  min-width: 44px;
+  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>