CodeRunner.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <template>
  2. <div class="code-runner" :class="{ running, hasOutput: output !== null }">
  3. <!-- Header -->
  4. <div class="cr-header">
  5. <span class="cr-lang">{{ language }}</span>
  6. <button class="cr-run-btn" :disabled="running" @click="runCode">
  7. <span v-if="!running" class="run-icon">▶</span>
  8. <span v-else class="spinner"></span>
  9. {{ running ? '运行中...' : '运行' }}
  10. </button>
  11. </div>
  12. <!-- Code: highlighted read-only -->
  13. <pre class="cr-code"><code v-html="highlightedCode"></code></pre>
  14. <!-- Iframe preview for HTML/CSS -->
  15. <Transition name="slide">
  16. <div v-if="isHtmlLike && iframeSrc !== null" class="cr-output cr-iframe-output">
  17. <div class="output-header">
  18. <span class="output-label">✓ 预览</span>
  19. <button class="output-close" @click="iframeSrc = null">✕</button>
  20. </div>
  21. <iframe class="preview-iframe" :srcdoc="iframeSrc" sandbox="allow-scripts"></iframe>
  22. </div>
  23. </Transition>
  24. <!-- Output for backend-executed languages -->
  25. <Transition name="slide">
  26. <div v-if="!isHtmlLike && output !== null" class="cr-output" :class="{ error: !success }">
  27. <div class="output-header">
  28. <span class="output-label">{{ success ? '✓ 输出' : '✗ 错误' }}</span>
  29. <button class="output-close" @click="output = null">✕</button>
  30. </div>
  31. <pre class="output-content"><code>{{ output }}</code></pre>
  32. </div>
  33. </Transition>
  34. </div>
  35. </template>
  36. <script setup lang="ts">
  37. import { ref, computed } from 'vue'
  38. import hljs from 'highlight.js'
  39. import axios from 'axios'
  40. const props = withDefaults(defineProps<{
  41. code: string
  42. language?: string
  43. }>(), {
  44. language: 'python',
  45. })
  46. const running = ref(false)
  47. const output = ref<string | null>(null)
  48. const success = ref(true)
  49. const iframeSrc = ref<string | null>(null)
  50. const isHtmlLike = computed(() => props.language === 'html' || props.language === 'css')
  51. const highlightedCode = computed(() => {
  52. try {
  53. if (props.language && hljs.getLanguage(props.language)) {
  54. return hljs.highlight(props.code, { language: props.language }).value
  55. }
  56. return hljs.highlightAuto(props.code).value
  57. } catch {
  58. return escapeHtml(props.code)
  59. }
  60. })
  61. function escapeHtml(text: string): string {
  62. return text.replace(/[&<>"']/g, (ch: string) => {
  63. const map: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
  64. return map[ch] || ch
  65. })
  66. }
  67. async function runCode() {
  68. if (running.value) return
  69. running.value = true
  70. output.value = null
  71. iframeSrc.value = null
  72. // HTML/CSS: render in iframe directly (no backend call)
  73. if (isHtmlLike.value) {
  74. if (props.language === 'html') {
  75. iframeSrc.value = props.code
  76. } else if (props.language === 'css') {
  77. iframeSrc.value = `<!DOCTYPE html>
  78. <html>
  79. <head>
  80. <style>${props.code}</style>
  81. </head>
  82. <body>
  83. <div class="preview-content" style="font-family:sans-serif;padding:20px;">
  84. <h1>CSS 预览</h1>
  85. <p>你的样式已应用到此页面。</p>
  86. </div>
  87. </body>
  88. </html>`
  89. }
  90. running.value = false
  91. return
  92. }
  93. // Backend execution for other languages
  94. try {
  95. const res = await axios.post('/api/code/execute', {
  96. code: props.code,
  97. language: props.language,
  98. })
  99. success.value = res.data.success
  100. output.value = res.data.output || res.data.error || '(无输出)'
  101. } catch (err: any) {
  102. success.value = false
  103. output.value = err.response?.data?.detail || err.message || '请求失败'
  104. } finally {
  105. running.value = false
  106. }
  107. }
  108. </script>
  109. <style scoped>
  110. .code-runner {
  111. margin: 12px 0;
  112. border: 1px solid #e4e7ed;
  113. border-radius: 10px;
  114. overflow: hidden;
  115. background: #1e1e1e;
  116. font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
  117. }
  118. .cr-header {
  119. display: flex;
  120. align-items: center;
  121. justify-content: space-between;
  122. padding: 8px 14px;
  123. background: #2d2d2d;
  124. border-bottom: 1px solid #3a3a3a;
  125. }
  126. .cr-lang {
  127. font-size: 11px;
  128. font-weight: 600;
  129. color: #999;
  130. text-transform: uppercase;
  131. letter-spacing: 0.5px;
  132. }
  133. .cr-run-btn {
  134. display: flex;
  135. align-items: center;
  136. gap: 6px;
  137. padding: 4px 14px;
  138. font-size: 12px;
  139. font-weight: 600;
  140. font-family: inherit;
  141. color: #fff;
  142. background: #2ea043;
  143. border: none;
  144. border-radius: 6px;
  145. cursor: pointer;
  146. transition: all 0.2s;
  147. }
  148. .cr-run-btn:hover:not(:disabled) {
  149. background: #2c974b;
  150. transform: translateY(-1px);
  151. }
  152. .cr-run-btn:disabled {
  153. opacity: 0.6;
  154. cursor: not-allowed;
  155. }
  156. .run-icon {
  157. font-size: 10px;
  158. }
  159. .spinner {
  160. width: 12px;
  161. height: 12px;
  162. border: 2px solid rgba(255,255,255,0.3);
  163. border-top-color: white;
  164. border-radius: 50%;
  165. animation: spin 0.6s linear infinite;
  166. }
  167. @keyframes spin {
  168. to { transform: rotate(360deg); }
  169. }
  170. .cr-code {
  171. margin: 0;
  172. padding: 14px 16px;
  173. font-size: 13px;
  174. line-height: 1.6;
  175. overflow-x: auto;
  176. color: #c9d1d9;
  177. }
  178. .cr-code :deep(code) {
  179. font-family: inherit;
  180. }
  181. /* Output */
  182. .cr-output {
  183. border-top: 1px solid #3a3a3a;
  184. background: #0d1117;
  185. }
  186. .cr-output.error {
  187. border-top-color: #5a1d1d;
  188. }
  189. .output-header {
  190. display: flex;
  191. align-items: center;
  192. justify-content: space-between;
  193. padding: 8px 14px;
  194. background: #161b22;
  195. }
  196. .output-label {
  197. font-size: 11px;
  198. font-weight: 600;
  199. color: #7ee787;
  200. }
  201. .cr-output.error .output-label {
  202. color: #ff7b72;
  203. }
  204. .output-close {
  205. background: none;
  206. border: none;
  207. color: #666;
  208. cursor: pointer;
  209. font-size: 14px;
  210. padding: 2px 6px;
  211. border-radius: 4px;
  212. }
  213. .output-close:hover {
  214. color: #fff;
  215. background: rgba(255,255,255,0.1);
  216. }
  217. .output-content {
  218. margin: 0;
  219. padding: 12px 16px;
  220. font-size: 13px;
  221. line-height: 1.5;
  222. color: #c9d1d9;
  223. max-height: 200px;
  224. overflow-y: auto;
  225. white-space: pre-wrap;
  226. word-break: break-all;
  227. }
  228. /* Iframe output */
  229. .cr-iframe-output {
  230. padding: 0;
  231. }
  232. .preview-iframe {
  233. width: 100%;
  234. min-height: 300px;
  235. border: none;
  236. background: #fff;
  237. display: block;
  238. }
  239. /* Transition */
  240. .slide-enter-active,
  241. .slide-leave-active {
  242. transition: all 0.25s ease;
  243. }
  244. .slide-enter-from,
  245. .slide-leave-to {
  246. opacity: 0;
  247. max-height: 0;
  248. }
  249. .slide-enter-to,
  250. .slide-leave-from {
  251. opacity: 1;
  252. max-height: 300px;
  253. }
  254. </style>