math-editor.blade.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. @php
  2. $gridColumns = $getColumns();
  3. $id = $getId();
  4. $label = $getLabel();
  5. $name = $getName();
  6. $statePath = $getStatePath();
  7. @endphp
  8. <x-dynamic-component component="filament-forms::field-wrapper"
  9. :field="$field"
  10. :grid-columns="$gridColumns"
  11. class="math-editor-field">
  12. <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  13. <!-- Input Section -->
  14. <div>
  15. @if ($label)
  16. <label for="{{ $id }}" class="block text-sm font-medium text-gray-700 mb-2">
  17. {{ $label }}
  18. </label>
  19. @endif
  20. <textarea
  21. id="{{ $id }}"
  22. name="{{ $name }}"
  23. type="text"
  24. class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-mono text-sm"
  25. @if ($isRequired()) required @endif
  26. @if ($isDisabled()) disabled @endif
  27. x-data="mathEditor({
  28. value: @js($getState()),
  29. statePath: '{{ $statePath }}',
  30. })"
  31. x-model="value"
  32. x-on:input="updateValue"
  33. x-on:change="updateValue"
  34. x-on:livewireUpdating="$event.target.value = value"
  35. x-on:livewireUpdated="$nextTick(() => { value = $event.detail.value; updateValue(); })"
  36. placeholder="Enter LaTeX code...
  37. Example:
  38. $f(x) = ax^2 + bx + c$
  39. $\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$"
  40. rows="{{ $getMaxHeight() ? min($getMaxHeight(), 20) : 8 }}"
  41. {{ $attributes->merge($getExtraAttributes())->class(['filament-forms-textarea-input']) }}
  42. >{{ $getState() }}</textarea>
  43. @error($statePath)
  44. <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
  45. @enderror
  46. @if ($hint = $getHint())
  47. <p class="mt-1 text-sm text-gray-500">{{ $hint }}</p>
  48. @endif
  49. </div>
  50. <!-- Preview Section -->
  51. <div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
  52. <h4 class="text-sm font-medium text-gray-700 mb-3">Preview</h4>
  53. <div class="min-h-[200px] p-4 bg-white rounded border overflow-auto">
  54. <div class="math-render math-preview">
  55. @if ($getState())
  56. {{ $getState() }}
  57. @else
  58. <span class="text-gray-400 text-sm">Enter LaTeX code to see preview...</span>
  59. @endif
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. </x-dynamic-component>
  65. @push('scripts')
  66. <script>
  67. document.addEventListener('alpine:init', () => {
  68. Alpine.data('mathEditor', (config = {}) => ({
  69. value: config.value || '',
  70. statePath: config.statePath || '',
  71. updateValue() {
  72. // 触发 Livewire 更新
  73. this.$wire.set(this.statePath, this.value);
  74. // 更新预览
  75. this.updatePreview();
  76. },
  77. updatePreview() {
  78. const previewElement = this.$el.closest('.math-editor-field').querySelector('.math-preview');
  79. if (previewElement && typeof window.renderMathElement === 'function') {
  80. previewElement.dataset.mathContent = this.value;
  81. window.renderMathElement(previewElement);
  82. }
  83. },
  84. init() {
  85. // 初始化预览
  86. this.$nextTick(() => {
  87. this.updatePreview();
  88. // 监听 Livewire 更新
  89. this.$el.addEventListener('livewire:updated', (e) => {
  90. this.value = e.detail.value;
  91. this.updatePreview();
  92. });
  93. });
  94. }
  95. }));
  96. });
  97. // 添加全局渲染函数
  98. window.renderMathElement = function(element) {
  99. if (typeof window.katex === 'undefined') {
  100. // 等待 KaTeX 加载
  101. setTimeout(() => {
  102. if (typeof window.renderMathElement === 'function') {
  103. window.renderMathElement(element);
  104. }
  105. }, 100);
  106. return;
  107. }
  108. const content = element.dataset.mathContent || element.textContent;
  109. if (!content) return;
  110. try {
  111. let html = content;
  112. // 渲染 $$...$$ 块级公式
  113. html = html.replace(/\$\$([\s\S]*?)\$\$/g, (match, formula) => {
  114. try {
  115. return window.katex.renderToString(formula.trim(), {
  116. throwOnError: false,
  117. displayMode: true
  118. });
  119. } catch (e) {
  120. return match;
  121. }
  122. });
  123. // 渲染 $...$ 行内公式
  124. html = html.replace(/\$(.*?)\$/g, (match, formula) => {
  125. try {
  126. return window.katex.renderToString(formula, {
  127. throwOnError: false,
  128. displayMode: false
  129. });
  130. } catch (e) {
  131. return match;
  132. }
  133. });
  134. // 渲染 \(...\) 行内公式
  135. html = html.replace(/\\\((.*?)\\\)/g, (match, formula) => {
  136. try {
  137. return window.katex.renderToString(formula, {
  138. throwOnError: false,
  139. displayMode: false
  140. });
  141. } catch (e) {
  142. return match;
  143. }
  144. });
  145. element.innerHTML = html;
  146. } catch (e) {
  147. console.error('Math render error:', e);
  148. }
  149. };
  150. // 页面加载完成后渲染所有数学元素
  151. document.addEventListener('DOMContentLoaded', () => {
  152. if (typeof window.katex === 'undefined') {
  153. const script = document.createElement('script');
  154. script.src = '/js/katex.min.js';
  155. script.onload = () => {
  156. document.querySelectorAll('.math-render').forEach(window.renderMathElement);
  157. };
  158. document.head.appendChild(script);
  159. } else {
  160. document.querySelectorAll('.math-render').forEach(window.renderMathElement);
  161. }
  162. });
  163. // Livewire 兼容性
  164. document.addEventListener('livewire:initialized', () => {
  165. document.querySelectorAll('.math-render').forEach(window.renderMathElement);
  166. });
  167. document.addEventListener('livewire:updated', () => {
  168. setTimeout(() => {
  169. document.querySelectorAll('.math-render').forEach(window.renderMathElement);
  170. }, 100);
  171. });
  172. </script>
  173. @endpush
  174. @push('styles')
  175. <link rel="stylesheet" href="/css/katex/katex.min.css">
  176. <style>
  177. .math-preview {
  178. word-wrap: break-word;
  179. line-height: 1.6;
  180. }
  181. .math-preview .katex {
  182. font-size: 1em;
  183. }
  184. </style>
  185. @endpush