|
27 | 27 | </a> |
28 | 28 | </div> |
29 | 29 |
|
30 | | - <div class="online-compiler-demo__split"> |
| 30 | + <!-- 默认态:只读源码 + "动手试一试" CTA。源码占满正文宽,不再两栏挤压 --> |
| 31 | + <div class="online-compiler-demo__inline"> |
| 32 | + <div v-if="armSourcePath" class="online-compiler-demo__source-tabs"> |
| 33 | + <button |
| 34 | + type="button" |
| 35 | + class="online-compiler-demo__button online-compiler-demo__button--secondary online-compiler-demo__button--tab" |
| 36 | + :class="{ 'is-active': displayKind !== 'arm' }" |
| 37 | + @click="switchSourceKind('default')" |
| 38 | + > |
| 39 | + 源码 |
| 40 | + </button> |
| 41 | + <button |
| 42 | + type="button" |
| 43 | + class="online-compiler-demo__button online-compiler-demo__button--secondary online-compiler-demo__button--tab" |
| 44 | + :class="{ 'is-active': displayKind === 'arm' }" |
| 45 | + @click="switchSourceKind('arm')" |
| 46 | + > |
| 47 | + ARM 精简源码 |
| 48 | + </button> |
| 49 | + </div> |
| 50 | + |
| 51 | + <div class="online-compiler-demo__source-view"> |
| 52 | + <div |
| 53 | + v-if="highlightedHtml" |
| 54 | + class="online-compiler-demo__source-highlight" |
| 55 | + v-html="highlightedHtml" |
| 56 | + /> |
| 57 | + <pre v-else class="online-compiler-demo__source-code"><code>{{ displaySource }}</code></pre> |
| 58 | + </div> |
| 59 | + |
| 60 | + <p |
| 61 | + v-if="sourceLoadState === 'loading' || sourceLoadState === 'error'" |
| 62 | + class="online-compiler-demo__source-hint" |
| 63 | + > |
| 64 | + {{ sourceLoadState === 'error' ? '源码加载失败,可点上方源码链接查看' : '加载源码中…' }} |
| 65 | + </p> |
| 66 | + |
| 67 | + <button class="online-compiler-demo__cta" type="button" @click="openModal"> |
| 68 | + <span class="online-compiler-demo__cta-text">动手试一试</span> |
| 69 | + <span class="online-compiler-demo__cta-arrow" aria-hidden="true">→</span> |
| 70 | + </button> |
| 71 | + </div> |
| 72 | + |
| 73 | + <!-- 浮层态:Teleport 到 body,灰色遮罩 + 比页面小一圈的卡片,完整 IDE --> |
| 74 | + <Teleport to="body"> |
| 75 | + <div |
| 76 | + v-if="modalOpen" |
| 77 | + class="online-compiler-demo__overlay" |
| 78 | + @click.self="closeModal" |
| 79 | + > |
| 80 | + <div |
| 81 | + class="online-compiler-demo__modal" |
| 82 | + role="dialog" |
| 83 | + aria-modal="true" |
| 84 | + aria-labelledby="ocd-modal-title" |
| 85 | + > |
| 86 | + <div class="online-compiler-demo__modal-header"> |
| 87 | + <div> |
| 88 | + <p class="online-compiler-demo__eyebrow">Compiler Explorer</p> |
| 89 | + <strong id="ocd-modal-title">{{ title }}</strong> |
| 90 | + </div> |
| 91 | + <button |
| 92 | + class="online-compiler-demo__modal-close" |
| 93 | + type="button" |
| 94 | + aria-label="关闭" |
| 95 | + @click="closeModal" |
| 96 | + >✕</button> |
| 97 | + </div> |
| 98 | + |
| 99 | + <div class="online-compiler-demo__split"> |
31 | 100 | <div class="online-compiler-demo__source-pane"> |
32 | 101 | <div v-if="armSourcePath" class="online-compiler-demo__source-tabs"> |
33 | 102 | <button |
|
56 | 125 | /> |
57 | 126 | <pre v-else class="online-compiler-demo__source-code"><code>{{ displaySource }}</code></pre> |
58 | 127 | </div> |
59 | | - <textarea |
60 | | - v-else |
61 | | - v-model="editorSource" |
62 | | - class="online-compiler-demo__textarea" |
63 | | - spellcheck="false" |
64 | | - autocomplete="off" |
65 | | - autocorrect="off" |
66 | | - autocapitalize="off" |
67 | | - /> |
| 128 | + <div v-else class="online-compiler-demo__editor-wrap"> |
| 129 | + <div |
| 130 | + ref="editorBackdropRef" |
| 131 | + class="online-compiler-demo__source-highlight online-compiler-demo__editor-backdrop" |
| 132 | + v-html="editorHighlightedHtml" |
| 133 | + /> |
| 134 | + <textarea |
| 135 | + ref="editorTextareaRef" |
| 136 | + v-model="editorSource" |
| 137 | + class="online-compiler-demo__textarea online-compiler-demo__editor-textarea" |
| 138 | + spellcheck="false" |
| 139 | + autocomplete="off" |
| 140 | + autocorrect="off" |
| 141 | + autocapitalize="off" |
| 142 | + @scroll="onEditorScroll" |
| 143 | + /> |
| 144 | + </div> |
68 | 145 |
|
69 | 146 | <p |
70 | 147 | v-if="!editorOpen && (sourceLoadState === 'loading' || sourceLoadState === 'error')" |
|
195 | 272 | </div> |
196 | 273 | <pre><code>{{ result.text }}</code></pre> |
197 | 274 | </div> |
| 275 | + <!-- 编译进行中(activeAction 置位、result 尚未返回):shimmer 骨架,避免结果区整块消失干等 --> |
| 276 | + <div v-else-if="activeAction" class="online-compiler-demo__result online-compiler-demo__result--skeleton"> |
| 277 | + <div class="online-compiler-demo__result-header"> |
| 278 | + <span class="skel skel--title" /> |
| 279 | + <span class="skel skel--meta" /> |
| 280 | + </div> |
| 281 | + <div class="online-compiler-demo__skel-lines"> |
| 282 | + <span |
| 283 | + v-for="(w, i) in [92, 78, 86, 58, 90, 72, 95, 48]" |
| 284 | + :key="i" |
| 285 | + class="skel skel--line" |
| 286 | + :style="{ width: w + '%' }" |
| 287 | + /> |
| 288 | + </div> |
| 289 | + </div> |
198 | 290 | </div> |
199 | 291 | </div> |
| 292 | + </div> |
| 293 | + </div> |
| 294 | + </Teleport> |
200 | 295 |
|
201 | 296 | <p v-if="error" class="online-compiler-demo__error"> |
202 | 297 | {{ error }} |
@@ -280,6 +375,10 @@ const error = ref('') |
280 | 375 | const result = ref<CompileResult | null>(null) |
281 | 376 | const editorOpen = ref(false) |
282 | 377 | const highlightedHtml = ref('') |
| 378 | +// 编辑态高亮(overlay 技巧):编辑框背后垫一层 shiki 高亮,文字透明,光标/选区由 textarea 接管 |
| 379 | +const editorHighlightedHtml = ref('') |
| 380 | +const editorBackdropRef = ref<HTMLElement | null>(null) |
| 381 | +const editorTextareaRef = ref<HTMLTextAreaElement | null>(null) |
283 | 382 | const optionsOpen = ref(false) |
284 | 383 | const editorSourceKind = ref<SourceKind>('default') |
285 | 384 | const editorSource = ref('') |
@@ -312,6 +411,26 @@ watch(displaySource, async (code) => { |
312 | 411 | } |
313 | 412 | }) |
314 | 413 |
|
| 414 | +// 编辑态:边打边重新高亮(overlay backdrop 跟随 editorSource) |
| 415 | +watch(editorSource, async (code) => { |
| 416 | + editorHighlightedHtml.value = '' |
| 417 | + if (!code) return |
| 418 | + try { |
| 419 | + editorHighlightedHtml.value = await highlightCpp(code) |
| 420 | + } catch { |
| 421 | + editorHighlightedHtml.value = '' |
| 422 | + } |
| 423 | +}) |
| 424 | +
|
| 425 | +// textarea 与 backdrop 滚动同步(两层重叠,滚动必须一致,否则错位) |
| 426 | +function onEditorScroll() { |
| 427 | + const ta = editorTextareaRef.value |
| 428 | + const bd = editorBackdropRef.value |
| 429 | + if (!ta || !bd) return |
| 430 | + bd.scrollTop = ta.scrollTop |
| 431 | + bd.scrollLeft = ta.scrollLeft |
| 432 | +} |
| 433 | +
|
315 | 434 | const actions = computed<DemoAction[]>(() => { |
316 | 435 | const available: DemoAction[] = [] |
317 | 436 | if (props.allowRun) { |
@@ -423,6 +542,33 @@ function closeEditor(): void { |
423 | 542 | editorOpen.value = false |
424 | 543 | } |
425 | 544 |
|
| 545 | +// ── 浮层(模态):默认只显源码,点"动手试一试"弹完整 IDE ── |
| 546 | +const modalOpen = ref(false) |
| 547 | +
|
| 548 | +function onModalEsc(e: KeyboardEvent) { |
| 549 | + if (e.key === 'Escape' && modalOpen.value) closeModal() |
| 550 | +} |
| 551 | +
|
| 552 | +async function openModal(): Promise<void> { |
| 553 | + modalOpen.value = true |
| 554 | + document.body.style.overflow = 'hidden' |
| 555 | + window.addEventListener('keydown', onModalEsc) |
| 556 | + await openEditor() // 加载可编辑源码、置 editorOpen=true |
| 557 | +} |
| 558 | +
|
| 559 | +function closeModal(): void { |
| 560 | + // closeEditor 把编辑内容回写源码缓存 + editorOpen=false |
| 561 | + closeEditor() |
| 562 | + modalOpen.value = false |
| 563 | + document.body.style.overflow = '' |
| 564 | + window.removeEventListener('keydown', onModalEsc) |
| 565 | +} |
| 566 | +
|
| 567 | +onBeforeUnmount(() => { |
| 568 | + window.removeEventListener('keydown', onModalEsc) |
| 569 | + if (modalOpen.value) document.body.style.overflow = '' |
| 570 | +}) |
| 571 | +
|
426 | 572 | async function resetEditor(): Promise<void> { |
427 | 573 | activeAction.value = 'source' |
428 | 574 | error.value = '' |
@@ -472,33 +618,43 @@ function stripAnsi(value: string): string { |
472 | 618 |
|
473 | 619 | function extractExecutionText(payload: any): string { |
474 | 620 | const exec = payload.execResult ?? payload.executionResult ?? payload |
475 | | - const chunks = [ |
476 | | - linesToText(exec.stdout), |
477 | | - linesToText(exec.stderr), |
478 | | - linesToText(payload.stdout), |
479 | | - linesToText(payload.stderr), |
480 | | - linesToText(payload.buildResult?.stdout), |
481 | | - linesToText(payload.buildResult?.stderr), |
482 | | - ].filter(Boolean) |
| 621 | + // godbolt executor 响应常把程序输出同时放在 execResult 和顶层(此时 exec===payload), |
| 622 | + // 每路只取第一份非空的,避免把同一份输出拼两遍。(对齐 C-Journey f85300b 修复) |
| 623 | + if (isCompilationFailure(payload, linesToText(payload.asm))) { |
| 624 | + const diag = gatherDiagnostics(payload) |
| 625 | + return diag |
| 626 | + ? `❌ 编译失败:\n${diag}` |
| 627 | + : '❌ 编译失败,但 Compiler Explorer 没有返回诊断信息。检查源码语法、编译器 id 与参数,或点「打开 Godbolt」看完整输出。' |
| 628 | + } |
| 629 | + const out = linesToText(exec.stdout) || linesToText(payload.stdout) || linesToText(payload.buildResult?.stdout) |
| 630 | + const err = linesToText(exec.stderr) || linesToText(payload.stderr) || linesToText(payload.buildResult?.stderr) |
| 631 | + const chunks = [out, err].filter(Boolean) |
| 632 | +
|
| 633 | + if (exec.code !== undefined && exec.code !== 0) chunks.push(`exit code: ${exec.code}`) |
| 634 | + else if (payload.code !== undefined && payload.code !== 0) chunks.push(`exit code: ${payload.code}`) |
| 635 | + return chunks.join('\n').trim() || '(程序无输出)' |
| 636 | +} |
483 | 637 |
|
484 | | - if (exec.code !== undefined) chunks.push(`exit code: ${exec.code}`) |
485 | | - return chunks.join('\n').trim() |
| 638 | +// 收集编译/运行诊断(execResult → 顶层 → buildResult,stderr 优先再 stdout),每路第一份非空避免重复 |
| 639 | +function gatherDiagnostics(payload: any): string { |
| 640 | + const exec = payload.execResult ?? payload.executionResult ?? payload |
| 641 | + const err = linesToText(exec.stderr) || linesToText(payload.stderr) || linesToText(payload.buildResult?.stderr) |
| 642 | + const out = linesToText(exec.stdout) || linesToText(payload.stdout) || linesToText(payload.buildResult?.stdout) |
| 643 | + return [err, out].filter(Boolean).join('\n') |
486 | 644 | } |
487 | 645 |
|
488 | 646 | function extractAsmText(payload: any): string { |
489 | 647 | const asm = linesToText(payload.asm) |
490 | | - const diagnostics = [ |
491 | | - linesToText(payload.stdout), |
492 | | - linesToText(payload.stderr), |
493 | | - linesToText(payload.buildResult?.stdout), |
494 | | - linesToText(payload.buildResult?.stderr), |
495 | | - ].filter(Boolean).join('\n') |
496 | | -
|
497 | 648 | if (isCompilationFailure(payload, asm)) { |
498 | | - return (diagnostics || asm || '编译失败,但 Compiler Explorer 没有返回诊断信息。').trim() |
| 649 | + // 顶层与 buildResult 可能同源,每路只取第一份非空。 |
| 650 | + const err = linesToText(payload.stderr) || linesToText(payload.buildResult?.stderr) |
| 651 | + const out = linesToText(payload.stdout) || linesToText(payload.buildResult?.stdout) |
| 652 | + const diag = [err, out].filter(Boolean).join('\n') |
| 653 | + return diag |
| 654 | + ? `❌ 编译失败:\n${diag}` |
| 655 | + : '❌ 编译失败,但 Compiler Explorer 没有返回诊断信息。检查源码语法、编译器 id 与参数,或点「打开 Godbolt」看完整输出。' |
499 | 656 | } |
500 | | -
|
501 | | - return (asm || diagnostics || 'Compiler Explorer 没有返回可显示的输出。').trim() |
| 657 | + return (asm || 'Compiler Explorer 没有返回可显示的汇编输出。').trim() |
502 | 658 | } |
503 | 659 |
|
504 | 660 | function isCompilationFailure(payload: any, asm: string): boolean { |
|
0 commit comments