smart-code-editor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +155 -0
  2. package/lib/adapters/vanilla/index.d.ts +3 -0
  3. package/lib/adapters/vanilla/index.d.ts.map +1 -0
  4. package/lib/config/languages.d.ts +21 -0
  5. package/lib/config/languages.d.ts.map +1 -0
  6. package/lib/config/runnerStrategies.d.ts +12 -0
  7. package/lib/config/runnerStrategies.d.ts.map +1 -0
  8. package/lib/config/runnerStrategies_v2.d.ts +31 -0
  9. package/lib/config/runnerStrategies_v2.d.ts.map +1 -0
  10. package/lib/config/themes.d.ts +54 -0
  11. package/lib/config/themes.d.ts.map +1 -0
  12. package/lib/core/BackendRunner.d.ts +78 -0
  13. package/lib/core/BackendRunner.d.ts.map +1 -0
  14. package/lib/core/CodeRunner.d.ts +32 -0
  15. package/lib/core/CodeRunner.d.ts.map +1 -0
  16. package/lib/core/LanguageManager.d.ts +41 -0
  17. package/lib/core/LanguageManager.d.ts.map +1 -0
  18. package/lib/core/LayoutManager.d.ts +59 -0
  19. package/lib/core/LayoutManager.d.ts.map +1 -0
  20. package/lib/core/MonacoWrapper.d.ts +63 -0
  21. package/lib/core/MonacoWrapper.d.ts.map +1 -0
  22. package/lib/core/SmartCodeEditor.d.ts +140 -0
  23. package/lib/core/SmartCodeEditor.d.ts.map +1 -0
  24. package/lib/dev-main.d.ts +2 -0
  25. package/lib/dev-main.d.ts.map +1 -0
  26. package/lib/index.cjs +242 -0
  27. package/lib/index.d.ts +5 -0
  28. package/lib/index.d.ts.map +1 -0
  29. package/lib/index.js +1369 -0
  30. package/lib/index.umd.cjs +242 -0
  31. package/lib/shims-vue.d.ts +4 -0
  32. package/lib/types/index.d.ts +101 -0
  33. package/lib/types/index.d.ts.map +1 -0
  34. package/lib/types/language.d.ts +37 -0
  35. package/lib/types/language.d.ts.map +1 -0
  36. package/lib/types/question.d.ts +75 -0
  37. package/lib/types/question.d.ts.map +1 -0
  38. package/lib/utils/loader.d.ts +9 -0
  39. package/lib/utils/loader.d.ts.map +1 -0
  40. package/lib/utils/markdown.d.ts +2 -0
  41. package/lib/utils/markdown.d.ts.map +1 -0
  42. package/package.json +72 -0
  43. package/src/adapters/vanilla/index.ts +7 -0
  44. package/src/adapters/vue/SmartCodeEditor.vue +1190 -0
  45. package/src/config/languages.ts +273 -0
  46. package/src/config/runnerStrategies.ts +261 -0
  47. package/src/config/runnerStrategies_v2.ts +182 -0
  48. package/src/config/themes.ts +37 -0
  49. package/src/core/BackendRunner.ts +329 -0
  50. package/src/core/CodeRunner.ts +107 -0
  51. package/src/core/LanguageManager.ts +108 -0
  52. package/src/core/LayoutManager.ts +268 -0
  53. package/src/core/MonacoWrapper.ts +173 -0
  54. package/src/core/SmartCodeEditor.ts +1015 -0
  55. package/src/dev-app.vue +488 -0
  56. package/src/dev-main.ts +7 -0
  57. package/src/index.ts +19 -0
  58. package/src/shims-vue.d.ts +4 -0
  59. package/src/types/index.ts +129 -0
  60. package/src/types/language.ts +44 -0
  61. package/src/types/question.ts +98 -0
  62. package/src/utils/loader.ts +69 -0
  63. package/src/utils/markdown.ts +89 -0
@@ -0,0 +1,1015 @@
1
+ import type {
2
+ SmartCodeEditorOptions,
3
+ RunResult,
4
+ LanguageConfig,
5
+ QuestionMarkdownOptions,
6
+ } from "../types";
7
+ import { MonacoWrapper } from "./MonacoWrapper";
8
+ import { LayoutManager } from "./LayoutManager";
9
+ import { LanguageManager } from "./LanguageManager";
10
+ import { CodeRunner } from "./CodeRunner";
11
+ import { debounce } from "../utils/loader";
12
+ import { renderMarkdown } from "../utils/markdown";
13
+
14
+ /**
15
+ * 智能代码编辑器主类
16
+ * 所有业务逻辑的入口点
17
+ */
18
+ export class SmartCodeEditor {
19
+ private container: HTMLElement;
20
+ private options: SmartCodeEditorOptions;
21
+
22
+ // 核心模块
23
+ private monacoWrapper: MonacoWrapper | null = null;
24
+ private layoutManager: LayoutManager | null = null;
25
+ private languageManager: LanguageManager;
26
+ private codeRunner: CodeRunner;
27
+
28
+ // 状态
29
+ private id: string;
30
+ private initialized: boolean = false;
31
+ private _pendingLanguage: string | null = null;
32
+ private _pendingValue: string | null = null;
33
+ private resizeObserver: ResizeObserver | null = null;
34
+ private loadingMask: HTMLElement | null = null;
35
+ private toolbar: HTMLElement | null = null;
36
+ private langSelect: HTMLSelectElement | null = null;
37
+ private markdownEditor: HTMLTextAreaElement | null = null;
38
+ private markdownPreview: HTMLElement | null = null;
39
+
40
+ constructor(options: SmartCodeEditorOptions) {
41
+ this.id = `sce_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
42
+ this.options = options;
43
+
44
+ // 获取容器
45
+ if (typeof options.container === "string") {
46
+ const el = document.querySelector(options.container);
47
+ if (!el) {
48
+ throw new Error(`Container "${options.container}" not found`);
49
+ }
50
+ this.container = el as HTMLElement;
51
+ } else {
52
+ this.container = options.container;
53
+ }
54
+
55
+ // 初始化管理器
56
+ this.languageManager = new LanguageManager(
57
+ options.language || "javascript",
58
+ );
59
+ this.codeRunner = new CodeRunner(options.runTimeout || 5000);
60
+
61
+ // 异步初始化
62
+ this.init();
63
+ }
64
+
65
+ /**
66
+ * 显示加载遮罩
67
+ */
68
+ private showLoading(): void {
69
+ if (this.loadingMask) return;
70
+
71
+ this.loadingMask = document.createElement("div");
72
+ this.loadingMask.className = "sce-loading-mask";
73
+ this.loadingMask.style.cssText = `
74
+ position: absolute;
75
+ top: 0;
76
+ left: 0;
77
+ width: 100%;
78
+ height: 100%;
79
+ background: ${this.options.theme === "vs-dark" ? "#1e1e1e" : "#ffffff"};
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ z-index: 1000;
84
+ color: ${this.options.theme === "vs-dark" ? "#d4d4d4" : "#333333"};
85
+ font-size: 14px;
86
+ font-family: -apple-system, system-ui, sans-serif;
87
+ `;
88
+
89
+ const spinner = document.createElement("div");
90
+ spinner.className = "sce-spinner";
91
+ spinner.textContent = "Loading...";
92
+
93
+ this.loadingMask.appendChild(spinner);
94
+ this.container.style.position = "relative"; // 确保容器有定位上下文
95
+ this.container.appendChild(this.loadingMask);
96
+ }
97
+
98
+ /**
99
+ * 隐藏加载遮罩
100
+ */
101
+ private hideLoading(): void {
102
+ if (this.loadingMask && this.loadingMask.parentNode) {
103
+ this.loadingMask.parentNode.removeChild(this.loadingMask);
104
+ this.loadingMask = null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * 创建 Markdown 题目面板
110
+ */
111
+ private createMarkdownPanel(options: QuestionMarkdownOptions): HTMLElement {
112
+ const wrapper = document.createElement("div");
113
+ wrapper.className = "sce-question-markdown";
114
+ wrapper.style.display = "flex";
115
+ wrapper.style.flexDirection = "column";
116
+ wrapper.style.height = "100%";
117
+ wrapper.style.minHeight = "0";
118
+ wrapper.style.gap = "12px";
119
+ wrapper.style.overflow = "hidden";
120
+
121
+ const editable = options.editable !== false;
122
+ const showEditor = editable && options.showEditor !== false;
123
+ const showPreview = editable ? options.showPreview !== false : true;
124
+ const initialValue = options.value || "";
125
+
126
+ let updatePreview: ((value: string) => void) | null = null;
127
+ let editorSection: HTMLDivElement | null = null;
128
+ let previewSection: HTMLDivElement | null = null;
129
+ let editorTab: HTMLButtonElement | null = null;
130
+ let previewTab: HTMLButtonElement | null = null;
131
+
132
+ const setTabState = () => {
133
+ if (editorSection && editorTab) {
134
+ const isVisible = editorSection.style.display !== "none";
135
+ editorTab.classList.toggle("active", isVisible);
136
+ }
137
+ if (previewSection && previewTab) {
138
+ const isVisible = previewSection.style.display !== "none";
139
+ previewTab.classList.toggle("active", isVisible);
140
+ }
141
+ };
142
+
143
+ const showOnly = (section: HTMLDivElement | null) => {
144
+ if (editorSection) {
145
+ editorSection.style.display =
146
+ section === editorSection ? "flex" : "none";
147
+ }
148
+ if (previewSection) {
149
+ previewSection.style.display =
150
+ section === previewSection ? "flex" : "none";
151
+ }
152
+ setTabState();
153
+ };
154
+
155
+ if (editable && showEditor && showPreview) {
156
+ const tabs = document.createElement("div");
157
+ tabs.className = "sce-markdown-tabs";
158
+ tabs.style.display = "flex";
159
+ tabs.style.gap = "8px";
160
+
161
+ editorTab = document.createElement("button");
162
+ editorTab.type = "button";
163
+ editorTab.className = "sce-markdown-tab active";
164
+ editorTab.textContent = "编辑";
165
+
166
+ previewTab = document.createElement("button");
167
+ previewTab.type = "button";
168
+ previewTab.className = "sce-markdown-tab active";
169
+ previewTab.textContent = "预览";
170
+
171
+ tabs.appendChild(editorTab);
172
+ tabs.appendChild(previewTab);
173
+ wrapper.appendChild(tabs);
174
+ }
175
+
176
+ if (showEditor) {
177
+ editorSection = document.createElement("div");
178
+ editorSection.className = "sce-markdown-editor-section";
179
+ editorSection.style.display = "flex";
180
+ editorSection.style.flexDirection = "column";
181
+ editorSection.style.flex = showPreview ? "1" : "1";
182
+ editorSection.style.minHeight = "0";
183
+
184
+ const editor = document.createElement("textarea");
185
+ editor.className = "sce-markdown-editor";
186
+ editor.value = initialValue;
187
+ editor.placeholder = options.placeholder || "在此编辑 Markdown...";
188
+ editor.readOnly = options.editable === false;
189
+ editor.style.width = "100%";
190
+ editor.style.flex = "1";
191
+ editor.style.minHeight = "120px";
192
+ editor.style.resize = "none";
193
+ editor.style.boxSizing = "border-box";
194
+ editor.style.padding = "10px";
195
+ editor.style.borderRadius = "6px";
196
+ editor.style.fontFamily =
197
+ "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
198
+ editor.style.fontSize = "12px";
199
+ editor.style.lineHeight = "1.5";
200
+
201
+ editorSection.appendChild(editor);
202
+ wrapper.appendChild(editorSection);
203
+
204
+ this.markdownEditor = editor;
205
+
206
+ const debouncedUpdate = debounce((value: string) => {
207
+ if (updatePreview) {
208
+ updatePreview(value);
209
+ }
210
+ if (options.onChange) {
211
+ options.onChange(value);
212
+ }
213
+ }, 150);
214
+
215
+ editor.addEventListener("input", () => {
216
+ debouncedUpdate(editor.value);
217
+ });
218
+ }
219
+
220
+ if (showPreview) {
221
+ previewSection = document.createElement("div");
222
+ previewSection.className = "sce-markdown-preview-section";
223
+ previewSection.style.display = "flex";
224
+ previewSection.style.flexDirection = "column";
225
+ previewSection.style.flex = "1";
226
+ previewSection.style.minHeight = "0";
227
+
228
+ const preview = document.createElement("div");
229
+ preview.className = "sce-markdown-preview";
230
+ preview.style.flex = "1";
231
+ preview.style.minHeight = "0";
232
+ preview.style.overflow = "auto";
233
+ preview.style.padding = "10px";
234
+ preview.style.borderRadius = "6px";
235
+ preview.style.fontFamily = "-apple-system, system-ui, sans-serif";
236
+ preview.style.fontSize = "13px";
237
+ preview.style.lineHeight = "1.6";
238
+
239
+ updatePreview = (value: string) => {
240
+ preview.innerHTML = renderMarkdown(value);
241
+ };
242
+ updatePreview(initialValue);
243
+
244
+ previewSection.appendChild(preview);
245
+ wrapper.appendChild(previewSection);
246
+
247
+ this.markdownPreview = preview;
248
+ }
249
+
250
+ if (editorTab && previewTab) {
251
+ editorTab.addEventListener("click", () => {
252
+ showOnly(editorSection);
253
+ });
254
+ previewTab.addEventListener("click", () => {
255
+ showOnly(previewSection);
256
+ });
257
+ // 默认显示编辑,隐藏预览
258
+ showOnly(editorSection);
259
+ }
260
+
261
+ if (!showEditor && !showPreview) {
262
+ const empty = document.createElement("div");
263
+ empty.textContent = "未启用 Markdown 编辑/预览";
264
+ wrapper.appendChild(empty);
265
+ }
266
+
267
+ return wrapper;
268
+ }
269
+
270
+ /**
271
+ * 初始化
272
+ */
273
+ private async init(): Promise<void> {
274
+ try {
275
+ this.showLoading();
276
+
277
+ // 1. 创建布局
278
+ if (this.options.showQuestionPanel !== false) {
279
+ this.layoutManager = new LayoutManager(
280
+ this.container,
281
+ this.options.defaultSplitRatio || 0.5,
282
+ this.options.theme || "vs-dark",
283
+ );
284
+
285
+ // 设置试题内容(Markdown 优先)
286
+ if (this.options.questionMarkdown) {
287
+ const markdownPanel = this.createMarkdownPanel(
288
+ this.options.questionMarkdown,
289
+ );
290
+ this.layoutManager.setLeftContent(markdownPanel);
291
+ this.layoutManager.setTheme(this.options.theme || "vs-dark");
292
+ } else if (this.options.questionContent) {
293
+ this.layoutManager.setLeftContent(this.options.questionContent);
294
+ }
295
+
296
+ // 使用右侧容器作为编辑器容器
297
+ const editorContainer = this.layoutManager.getRightContainer();
298
+
299
+ // 创建 Monaco 编辑器专用容器,确保 flex 布局正确
300
+ const monacoContainer = document.createElement("div");
301
+ monacoContainer.className = "sce-monaco-container";
302
+ monacoContainer.style.flex = "1";
303
+ monacoContainer.style.minHeight = "0";
304
+ monacoContainer.style.overflow = "hidden";
305
+ monacoContainer.style.position = "relative";
306
+ editorContainer.appendChild(monacoContainer);
307
+
308
+ this.monacoWrapper = new MonacoWrapper(monacoContainer);
309
+ } else {
310
+ // 不显示试题面板,但仍需确保 flex 布局
311
+ this.container.style.display = "flex";
312
+ this.container.style.flexDirection = "column";
313
+
314
+ const monacoContainer = document.createElement("div");
315
+ monacoContainer.className = "sce-monaco-container";
316
+ monacoContainer.style.flex = "1";
317
+ monacoContainer.style.minHeight = "0";
318
+ monacoContainer.style.overflow = "hidden";
319
+ monacoContainer.style.position = "relative";
320
+ this.container.appendChild(monacoContainer);
321
+
322
+ this.monacoWrapper = new MonacoWrapper(monacoContainer);
323
+ }
324
+
325
+ // 2. 初始化 Monaco Editor
326
+ const currentLang = this.languageManager.getCurrentLanguage();
327
+
328
+ // 获取初始代码:优先使用 options.value,否则使用模板
329
+ // 注意:使用 !== undefined 而不是 ||,因为空字符串是有效值
330
+ const initialValue =
331
+ this.options.value !== undefined && this.options.value !== null
332
+ ? this.options.value
333
+ : this.languageManager.getTemplate(
334
+ currentLang.id,
335
+ this.options.questionConfig,
336
+ this.options.languageTemplates,
337
+ );
338
+
339
+ console.log("🎨 SmartCodeEditor 初始化:", {
340
+ hasValue: this.options.value !== undefined,
341
+ valueLength: this.options.value?.length,
342
+ hasQuestionConfig: !!this.options.questionConfig,
343
+ hasLanguageTemplates: !!this.options.languageTemplates,
344
+ initialValuePreview: initialValue?.substring(0, 50) + "...",
345
+ });
346
+
347
+ await this.monacoWrapper.initialize({
348
+ value: initialValue,
349
+ language: currentLang.monacoId,
350
+ theme: this.options.theme || "vs-dark",
351
+ readOnly: this.options.readOnly || false,
352
+ automaticLayout: false, // Explicitly disable to avoid ResizeObserver loops with our own observer
353
+ suggestOnTriggerCharacters: this.options.suggestOnTriggerCharacters,
354
+ quickSuggestions: this.options.quickSuggestions,
355
+ });
356
+
357
+ // 确保初始模型也有正确的文件扩展名 URI
358
+ const ext = currentLang.extensions ? currentLang.extensions[0] : "";
359
+ this.monacoWrapper.updateModel(
360
+ initialValue,
361
+ currentLang.monacoId,
362
+ ext,
363
+ this.id,
364
+ );
365
+
366
+ // Check if language changed during initialization
367
+ const actualCurrentLang = this.languageManager.getCurrentLanguage();
368
+ if (actualCurrentLang.id !== currentLang.id) {
369
+ const ext = actualCurrentLang.extensions
370
+ ? actualCurrentLang.extensions[0]
371
+ : "";
372
+ this.monacoWrapper.updateModel(
373
+ this.getValue(),
374
+ actualCurrentLang.monacoId,
375
+ ext,
376
+ this.id,
377
+ );
378
+ }
379
+
380
+ // Check for pending value
381
+ if (this._pendingValue !== null) {
382
+ this.monacoWrapper.setValue(this._pendingValue);
383
+ this._pendingValue = null;
384
+ }
385
+
386
+ // 3. 监听内容变化
387
+ if (this.options.onChange) {
388
+ const debouncedOnChange = debounce(this.options.onChange, 300);
389
+ this.monacoWrapper.onDidChangeContent(debouncedOnChange);
390
+ }
391
+
392
+ this.initialized = true;
393
+
394
+ // Apply pending language if any
395
+ if (this._pendingLanguage) {
396
+ this.setLanguage(this._pendingLanguage);
397
+ this._pendingLanguage = null;
398
+ }
399
+
400
+ // Check for pending value (apply last to overwrite template from setLanguage)
401
+ if (this._pendingValue !== null) {
402
+ this.monacoWrapper.setValue(this._pendingValue);
403
+ this._pendingValue = null;
404
+ }
405
+
406
+ // 4. 创建工具栏
407
+ this.createToolbar();
408
+
409
+ // Monitor container resize
410
+ if (typeof ResizeObserver !== "undefined") {
411
+ this.resizeObserver = new ResizeObserver(() => {
412
+ requestAnimationFrame(() => {
413
+ this.layout();
414
+ });
415
+ });
416
+ this.resizeObserver.observe(this.container);
417
+ }
418
+
419
+ // Initial layout
420
+ this.layout();
421
+ } catch (error) {
422
+ console.error("Failed to initialize SmartCodeEditor:", error);
423
+ throw error;
424
+ } finally {
425
+ this.hideLoading();
426
+ }
427
+ }
428
+
429
+ /**
430
+ * 创建工具栏
431
+ */
432
+ private createToolbar(): void {
433
+ if (!this.monacoWrapper) return;
434
+
435
+ const editorContainer = this.layoutManager
436
+ ? this.layoutManager.getRightContainer()
437
+ : this.container;
438
+
439
+ // 创建工具栏容器
440
+ const toolbar = document.createElement("div");
441
+ this.toolbar = toolbar;
442
+ toolbar.className = "sce-toolbar";
443
+
444
+ const isDark = this.options.theme === "vs-dark";
445
+ const bg = isDark ? "#2d2d30" : "#fff";
446
+ const border = isDark ? "#454545" : "#f3f3f3";
447
+
448
+ toolbar.style.cssText = `
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 12px;
452
+ padding: 8px 16px;
453
+ background: ${bg};
454
+ border-bottom: 1px solid ${border};
455
+ transition: all 0.2s;
456
+ flex-shrink: 0;
457
+ `;
458
+
459
+ // 语言选择器
460
+ if (this.options.enableLanguageSwitch !== false) {
461
+ const langSelect = this.createLanguageSelector();
462
+ toolbar.appendChild(langSelect);
463
+ }
464
+
465
+ // 运行按钮
466
+ if (this.options.enableRun !== false) {
467
+ const runButton = this.createRunButton();
468
+ toolbar.appendChild(runButton);
469
+ }
470
+
471
+ // 提交按钮
472
+ if (this.options.enableSubmit !== false) {
473
+ const submitButton = this.createSubmitButton();
474
+ toolbar.appendChild(submitButton);
475
+ }
476
+
477
+ // 插入工具栏到编辑器容器前面
478
+ editorContainer.insertBefore(toolbar, editorContainer.firstChild);
479
+ }
480
+
481
+ /**
482
+ * 创建语言选择器
483
+ */
484
+ private createLanguageSelector(): HTMLElement {
485
+ const container = document.createElement("div");
486
+ container.style.display = "flex";
487
+ container.style.alignItems = "center";
488
+ container.style.gap = "8px";
489
+
490
+ const label = document.createElement("span");
491
+ label.textContent = "语言:";
492
+ label.style.color = this.options.theme === "vs-dark" ? "#ccc" : "#666";
493
+ label.style.fontSize = "13px";
494
+
495
+ const select = document.createElement("select");
496
+ this.langSelect = select;
497
+
498
+ const isDark = this.options.theme === "vs-dark";
499
+ const bg = isDark ? "#3c3c3c" : "#ffffff";
500
+ const color = isDark ? "#ccc" : "#333";
501
+ const border = isDark ? "#454545" : "#ccc";
502
+
503
+ select.style.cssText = `
504
+ padding: 4px 8px;
505
+ border: 1px solid ${border};
506
+ background: ${bg};
507
+ color: ${color};
508
+ border-radius: 4px;
509
+ cursor: pointer;
510
+ font-size: 13px;
511
+ `;
512
+
513
+ // 添加语言选项
514
+ let languages = this.languageManager.getAllLanguages();
515
+ if (
516
+ this.options.supportedLanguages &&
517
+ this.options.supportedLanguages.length > 0
518
+ ) {
519
+ languages = languages.filter((lang) =>
520
+ this.options.supportedLanguages!.includes(lang.id),
521
+ );
522
+ }
523
+
524
+ languages.forEach((lang) => {
525
+ const option = document.createElement("option");
526
+ option.value = lang.id;
527
+ option.textContent = `${lang.icon || ""} ${lang.name}`.trim();
528
+ if (lang.id === this.languageManager.getCurrentLanguage().id) {
529
+ option.selected = true;
530
+ }
531
+ select.appendChild(option);
532
+ });
533
+
534
+ // 监听变化
535
+ select.addEventListener("change", () => {
536
+ this.setLanguage(select.value);
537
+ });
538
+
539
+ container.appendChild(label);
540
+ container.appendChild(select);
541
+ return container;
542
+ }
543
+
544
+ /**
545
+ * 创建运行按钮
546
+ */
547
+ private createRunButton(): HTMLElement {
548
+ const button = document.createElement("button");
549
+ button.innerHTML = `<span>▶</span> 运行`;
550
+ button.style.cssText = `
551
+ padding: 6px 16px;
552
+ border: none;
553
+ background: #0e639c;
554
+ color: white;
555
+ border-radius: 4px;
556
+ cursor: pointer;
557
+ font-size: 13px;
558
+ font-weight: 500;
559
+ margin-left: auto;
560
+ display: flex;
561
+ align-items: center;
562
+ gap: 6px;
563
+ transition: background 0.2s;
564
+ `;
565
+
566
+ button.addEventListener("click", async () => {
567
+ const originalContent = button.innerHTML;
568
+ try {
569
+ button.disabled = true;
570
+ button.style.background = "#0e639c80";
571
+ button.style.cursor = "not-allowed";
572
+ button.innerHTML = `<span class="sce-btn-spinner"></span> 运行中...`;
573
+
574
+ await this.run();
575
+ } finally {
576
+ button.disabled = false;
577
+ button.style.background = "#0e639c";
578
+ button.style.cursor = "pointer";
579
+ button.innerHTML = originalContent;
580
+ }
581
+ });
582
+
583
+ button.addEventListener("mouseenter", () => {
584
+ if (!button.disabled) button.style.background = "#1177bb";
585
+ });
586
+
587
+ button.addEventListener("mouseleave", () => {
588
+ if (!button.disabled) button.style.background = "#0e639c";
589
+ });
590
+
591
+ // Inject spinner style if not exists
592
+ this.injectSpinnerStyle();
593
+
594
+ return button;
595
+ }
596
+
597
+ /**
598
+ * 创建提交按钮
599
+ */
600
+ private createSubmitButton(): HTMLElement {
601
+ const button = document.createElement("button");
602
+ button.textContent = "提交";
603
+ button.title = "提交代码进行评测";
604
+ button.style.cssText = `
605
+ padding: 6px 16px;
606
+ border: none;
607
+ background: #4caf50;
608
+ color: white;
609
+ border-radius: 4px;
610
+ cursor: pointer;
611
+ font-size: 13px;
612
+ font-weight: 500;
613
+ margin-left: 8px;
614
+ display: flex;
615
+ align-items: center;
616
+ gap: 6px;
617
+ transition: background 0.2s;
618
+ `;
619
+
620
+ button.addEventListener("click", async () => {
621
+ if (this.options.onSubmit) {
622
+ const originalContent = button.textContent || "提交";
623
+ try {
624
+ button.disabled = true;
625
+ button.style.background = "#4caf5080";
626
+ button.style.cursor = "not-allowed";
627
+ button.innerHTML = `<span class="sce-btn-spinner"></span> 提交中...`;
628
+
629
+ await this.options.onSubmit(this.getValue(), this.getLanguage());
630
+ } finally {
631
+ button.disabled = false;
632
+ button.style.background = "#4caf50";
633
+ button.style.cursor = "pointer";
634
+ button.textContent = originalContent;
635
+ }
636
+ }
637
+ });
638
+
639
+ button.addEventListener("mouseenter", () => {
640
+ if (!button.disabled) button.style.background = "#43a047";
641
+ });
642
+
643
+ button.addEventListener("mouseleave", () => {
644
+ if (!button.disabled) button.style.background = "#4caf50";
645
+ });
646
+
647
+ return button;
648
+ }
649
+
650
+ private injectSpinnerStyle() {
651
+ if (document.getElementById("sce-btn-spinner-style")) return;
652
+ const style = document.createElement("style");
653
+ style.id = "sce-btn-spinner-style";
654
+ style.innerHTML = `
655
+ @keyframes sce-spin {
656
+ 0% { transform: rotate(0deg); }
657
+ 100% { transform: rotate(360deg); }
658
+ }
659
+ .sce-btn-spinner {
660
+ display: inline-block;
661
+ width: 12px;
662
+ height: 12px;
663
+ border: 2px solid rgba(255,255,255,0.3);
664
+ border-radius: 50%;
665
+ border-top-color: #fff;
666
+ animation: sce-spin 1s ease-in-out infinite;
667
+ }
668
+ `;
669
+ document.head.appendChild(style);
670
+ }
671
+
672
+ /**
673
+ * 获取代码
674
+ */
675
+ getValue(): string {
676
+ return this.monacoWrapper?.getValue() || "";
677
+ }
678
+
679
+ /**
680
+ * 设置代码
681
+ */
682
+ setValue(value: string): void {
683
+ if (!this.monacoWrapper || !this.initialized) {
684
+ this._pendingValue = value;
685
+ return;
686
+ }
687
+ this.monacoWrapper.setValue(value);
688
+ }
689
+
690
+ /**
691
+ * 获取当前语言
692
+ */
693
+ getLanguage(): string {
694
+ return this.languageManager.getCurrentLanguage().id;
695
+ }
696
+
697
+ /**
698
+ * 设置语言
699
+ */
700
+ setLanguage(language: string): void {
701
+ if (!this.monacoWrapper || !this.initialized) {
702
+ this._pendingLanguage = language;
703
+ return;
704
+ }
705
+ const lang = this.languageManager.switchLanguage(language);
706
+
707
+ // 获取当前代码和文件扩展名
708
+ const code = this.getValue();
709
+ const ext = lang.extensions ? lang.extensions[0] : "";
710
+
711
+ this.monacoWrapper?.updateModel(code, lang.monacoId, ext, this.id);
712
+
713
+ // 切换语言时,自动加载对应的模板代码
714
+ const template = this.languageManager.getTemplate(
715
+ language,
716
+ this.options.questionConfig,
717
+ this.options.languageTemplates,
718
+ );
719
+ this.setValue(template);
720
+
721
+ // 触发回调
722
+ if (this.options.onLanguageChange) {
723
+ this.options.onLanguageChange(language);
724
+ }
725
+ }
726
+
727
+ /**
728
+ * 获取支持的语言列表
729
+ */
730
+ getSupportedLanguages(): LanguageConfig[] {
731
+ return this.languageManager.getAllLanguages();
732
+ }
733
+
734
+ /**
735
+ * 设置主题
736
+ */
737
+ setTheme(theme: string): void {
738
+ this.monacoWrapper?.setTheme(theme);
739
+ this.layoutManager?.setTheme(theme);
740
+ this.options.theme = theme; // 更新 options 中的 theme
741
+
742
+ // 更新工具栏样式
743
+ if (this.toolbar) {
744
+ const isDark = theme === "vs-dark";
745
+ this.toolbar.style.background = isDark ? "#2d2d30" : "#f3f3f3";
746
+ this.toolbar.style.borderBottom = isDark
747
+ ? "1px solid #454545"
748
+ : "1px solid #e0e0e0";
749
+ }
750
+
751
+ // 更新语言选择器样式
752
+ if (this.langSelect) {
753
+ const isDark = theme === "vs-dark";
754
+ this.langSelect.style.background = isDark ? "#3c3c3c" : "#ffffff";
755
+ this.langSelect.style.color = isDark ? "#ccc" : "#333";
756
+ this.langSelect.style.borderColor = isDark ? "#454545" : "#ccc";
757
+
758
+ // 更新 label 颜色 (它是 select 的前一个兄弟节点)
759
+ if (this.langSelect.previousElementSibling instanceof HTMLElement) {
760
+ this.langSelect.previousElementSibling.style.color = isDark
761
+ ? "#ccc"
762
+ : "#666";
763
+ }
764
+ }
765
+ }
766
+
767
+ /**
768
+ * 获取主题
769
+ */
770
+ getTheme(): string {
771
+ return this.options.theme || "vs-dark";
772
+ }
773
+
774
+ /**
775
+ * 设置试题内容
776
+ */
777
+ setQuestionContent(content: string): void {
778
+ this.layoutManager?.setLeftContent(content);
779
+ this.markdownEditor = null;
780
+ this.markdownPreview = null;
781
+ }
782
+
783
+ /**
784
+ * 设置 Markdown 题目内容
785
+ */
786
+ setQuestionMarkdown(
787
+ value: string,
788
+ options?: Partial<QuestionMarkdownOptions>,
789
+ ): void {
790
+ if (!this.layoutManager) return;
791
+
792
+ const nextOptions: QuestionMarkdownOptions = {
793
+ ...(this.options.questionMarkdown || {}),
794
+ ...(options || {}),
795
+ value,
796
+ };
797
+
798
+ const panel = this.createMarkdownPanel(nextOptions);
799
+ this.layoutManager.setLeftContent(panel);
800
+ this.options.questionMarkdown = nextOptions;
801
+ this.layoutManager.setTheme(this.options.theme || "vs-dark");
802
+ }
803
+
804
+ /**
805
+ * 显示试题面板
806
+ */
807
+ showQuestionPanel(): void {
808
+ this.layoutManager?.setQuestionPanelVisible(true);
809
+ }
810
+
811
+ /**
812
+ * 隐藏试题面板
813
+ */
814
+ hideQuestionPanel(): void {
815
+ this.layoutManager?.setQuestionPanelVisible(false);
816
+ }
817
+
818
+ private testCases: import("../types").TestCase[] = [];
819
+
820
+ /**
821
+ * 设置测试用例
822
+ */
823
+ setTestCases(testCases: import("../types").TestCase[]): void {
824
+ this.testCases = testCases;
825
+ }
826
+
827
+ /**
828
+ * 运行代码
829
+ */
830
+ async run(testCases?: import("../types").TestCase[]): Promise<RunResult> {
831
+ const code = this.getValue();
832
+ const language = this.getLanguage();
833
+ const currentLang = this.languageManager.getCurrentLanguage();
834
+
835
+ if (!currentLang.canRun) {
836
+ const result: RunResult = {
837
+ output: "",
838
+ error: `语言 "${currentLang.name}" 不支持运行`,
839
+ executionTime: 0,
840
+ status: "error",
841
+ };
842
+
843
+ if (this.options.onRun) {
844
+ this.options.onRun(result);
845
+ }
846
+
847
+ return result;
848
+ }
849
+
850
+ // Use custom runner if provided
851
+ if (this.options.customRunner) {
852
+ // 优先使用传入的 testCases,如果没有则使用内部存储的
853
+ const casesToRun = testCases || this.testCases;
854
+
855
+ try {
856
+ const result = await this.options.customRunner(
857
+ code,
858
+ language,
859
+ casesToRun,
860
+ );
861
+
862
+ if (this.options.onRun) {
863
+ this.options.onRun(result);
864
+ }
865
+
866
+ return result;
867
+ } catch (error: any) {
868
+ const errorResult: RunResult = {
869
+ output: "",
870
+ error: error.message || "自定义运行器执行失败",
871
+ executionTime: 0,
872
+ status: "error",
873
+ };
874
+
875
+ if (this.options.onRun) {
876
+ this.options.onRun(errorResult);
877
+ }
878
+
879
+ return errorResult;
880
+ }
881
+ }
882
+
883
+ try {
884
+ // 优先使用传入的 testCases,如果没有则使用内部存储的
885
+ const casesToRun = testCases || this.testCases;
886
+ const result = await this.codeRunner.run(code, language, casesToRun);
887
+
888
+ // 触发回调
889
+ if (this.options.onRun) {
890
+ this.options.onRun(result);
891
+ }
892
+
893
+ return result;
894
+ } catch (error: any) {
895
+ const result: RunResult = {
896
+ output: "",
897
+ error: error.message,
898
+ executionTime: 0,
899
+ status: "error",
900
+ };
901
+
902
+ if (this.options.onRun) {
903
+ this.options.onRun(result);
904
+ }
905
+
906
+ return result;
907
+ }
908
+ }
909
+
910
+ /**
911
+ * 取消运行
912
+ */
913
+ cancelRun(): void {
914
+ this.codeRunner.cancel();
915
+ }
916
+
917
+ /**
918
+ * 调整布局
919
+ */
920
+ resize(): void {
921
+ this.monacoWrapper?.layout();
922
+ }
923
+
924
+ /**
925
+ * 设置分割比例
926
+ */
927
+ setSplitRatio(ratio: number): void {
928
+ this.layoutManager?.setSplitRatio(ratio);
929
+ this.monacoWrapper?.layout();
930
+ }
931
+
932
+ /**
933
+ * 重新布局
934
+ */
935
+ layout(): void {
936
+ this.monacoWrapper?.layout();
937
+ }
938
+
939
+ /**
940
+ * 更新配置
941
+ */
942
+ updateConfig(config: Partial<SmartCodeEditorOptions>): void {
943
+ const monacoOptions: any = {};
944
+
945
+ if (config.questionConfig) {
946
+ this.options.questionConfig = config.questionConfig;
947
+ }
948
+ if (config.languageTemplates) {
949
+ this.options.languageTemplates = config.languageTemplates;
950
+ }
951
+ if (config.supportedLanguages) {
952
+ this.options.supportedLanguages = config.supportedLanguages;
953
+ this.refreshLanguageSelector();
954
+ }
955
+
956
+ // Update suggestion options if provided
957
+ if (config.suggestOnTriggerCharacters !== undefined) {
958
+ this.options.suggestOnTriggerCharacters =
959
+ config.suggestOnTriggerCharacters;
960
+ monacoOptions.suggestOnTriggerCharacters =
961
+ config.suggestOnTriggerCharacters;
962
+ }
963
+
964
+ if (config.quickSuggestions !== undefined) {
965
+ this.options.quickSuggestions = config.quickSuggestions;
966
+ monacoOptions.quickSuggestions = config.quickSuggestions;
967
+ }
968
+
969
+ // Apply updates to monaco editor if we have any relevant changes
970
+ if (Object.keys(monacoOptions).length > 0) {
971
+ this.monacoWrapper?.updateOptions(monacoOptions);
972
+ }
973
+ }
974
+
975
+ /**
976
+ * 刷新语言选择器
977
+ */
978
+ private refreshLanguageSelector(): void {
979
+ if (!this.langSelect) return;
980
+
981
+ // 清空现有选项
982
+ this.langSelect.innerHTML = "";
983
+
984
+ // 重新添加选项
985
+ let languages = this.languageManager.getAllLanguages();
986
+ if (
987
+ this.options.supportedLanguages &&
988
+ this.options.supportedLanguages.length > 0
989
+ ) {
990
+ languages = languages.filter((lang) =>
991
+ this.options.supportedLanguages!.includes(lang.id),
992
+ );
993
+ }
994
+
995
+ languages.forEach((lang) => {
996
+ const option = document.createElement("option");
997
+ option.value = lang.id;
998
+ option.textContent = `${lang.icon || ""} ${lang.name}`.trim();
999
+ if (lang.id === this.languageManager.getCurrentLanguage().id) {
1000
+ option.selected = true;
1001
+ }
1002
+ this.langSelect!.appendChild(option);
1003
+ });
1004
+ }
1005
+
1006
+ /**
1007
+ * 销毁
1008
+ */
1009
+ destroy(): void {
1010
+ this.resizeObserver?.disconnect();
1011
+ this.monacoWrapper?.dispose();
1012
+ this.layoutManager?.destroy();
1013
+ this.codeRunner.destroy();
1014
+ }
1015
+ }