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.
- package/README.md +155 -0
- package/lib/adapters/vanilla/index.d.ts +3 -0
- package/lib/adapters/vanilla/index.d.ts.map +1 -0
- package/lib/config/languages.d.ts +21 -0
- package/lib/config/languages.d.ts.map +1 -0
- package/lib/config/runnerStrategies.d.ts +12 -0
- package/lib/config/runnerStrategies.d.ts.map +1 -0
- package/lib/config/runnerStrategies_v2.d.ts +31 -0
- package/lib/config/runnerStrategies_v2.d.ts.map +1 -0
- package/lib/config/themes.d.ts +54 -0
- package/lib/config/themes.d.ts.map +1 -0
- package/lib/core/BackendRunner.d.ts +78 -0
- package/lib/core/BackendRunner.d.ts.map +1 -0
- package/lib/core/CodeRunner.d.ts +32 -0
- package/lib/core/CodeRunner.d.ts.map +1 -0
- package/lib/core/LanguageManager.d.ts +41 -0
- package/lib/core/LanguageManager.d.ts.map +1 -0
- package/lib/core/LayoutManager.d.ts +59 -0
- package/lib/core/LayoutManager.d.ts.map +1 -0
- package/lib/core/MonacoWrapper.d.ts +63 -0
- package/lib/core/MonacoWrapper.d.ts.map +1 -0
- package/lib/core/SmartCodeEditor.d.ts +140 -0
- package/lib/core/SmartCodeEditor.d.ts.map +1 -0
- package/lib/dev-main.d.ts +2 -0
- package/lib/dev-main.d.ts.map +1 -0
- package/lib/index.cjs +242 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1369 -0
- package/lib/index.umd.cjs +242 -0
- package/lib/shims-vue.d.ts +4 -0
- package/lib/types/index.d.ts +101 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/language.d.ts +37 -0
- package/lib/types/language.d.ts.map +1 -0
- package/lib/types/question.d.ts +75 -0
- package/lib/types/question.d.ts.map +1 -0
- package/lib/utils/loader.d.ts +9 -0
- package/lib/utils/loader.d.ts.map +1 -0
- package/lib/utils/markdown.d.ts +2 -0
- package/lib/utils/markdown.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/adapters/vanilla/index.ts +7 -0
- package/src/adapters/vue/SmartCodeEditor.vue +1190 -0
- package/src/config/languages.ts +273 -0
- package/src/config/runnerStrategies.ts +261 -0
- package/src/config/runnerStrategies_v2.ts +182 -0
- package/src/config/themes.ts +37 -0
- package/src/core/BackendRunner.ts +329 -0
- package/src/core/CodeRunner.ts +107 -0
- package/src/core/LanguageManager.ts +108 -0
- package/src/core/LayoutManager.ts +268 -0
- package/src/core/MonacoWrapper.ts +173 -0
- package/src/core/SmartCodeEditor.ts +1015 -0
- package/src/dev-app.vue +488 -0
- package/src/dev-main.ts +7 -0
- package/src/index.ts +19 -0
- package/src/shims-vue.d.ts +4 -0
- package/src/types/index.ts +129 -0
- package/src/types/language.ts +44 -0
- package/src/types/question.ts +98 -0
- package/src/utils/loader.ts +69 -0
- package/src/utils/markdown.ts +89 -0
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="wrapperRef" class="sce-wrapper">
|
|
3
|
+
<!-- Monaco Editor Container - terminal panel will be moved here -->
|
|
4
|
+
<div ref="containerRef" class="sce-editor-area">
|
|
5
|
+
<!-- Terminal Panel (will be moved here after mount) -->
|
|
6
|
+
<div
|
|
7
|
+
ref="terminalPanelRef"
|
|
8
|
+
class="terminal-panel"
|
|
9
|
+
:class="{ collapsed: !isPanelExpanded }"
|
|
10
|
+
:style="isPanelExpanded ? { height: panelHeight + 'px' } : {}"
|
|
11
|
+
>
|
|
12
|
+
<!-- Resize Handle -->
|
|
13
|
+
<div class="resize-handle" @mousedown="startResize"></div>
|
|
14
|
+
|
|
15
|
+
<!-- Panel Tab Header -->
|
|
16
|
+
<div class="panel-tabs">
|
|
17
|
+
<div
|
|
18
|
+
class="tab-item"
|
|
19
|
+
:class="{ active: activeTab === 'testcase' }"
|
|
20
|
+
@click="
|
|
21
|
+
activeTab = 'testcase';
|
|
22
|
+
isPanelExpanded = true;
|
|
23
|
+
"
|
|
24
|
+
>
|
|
25
|
+
<span class="text">Testcase</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div
|
|
28
|
+
class="tab-item"
|
|
29
|
+
:class="{ active: activeTab === 'result' }"
|
|
30
|
+
@click="
|
|
31
|
+
activeTab = 'result';
|
|
32
|
+
isPanelExpanded = true;
|
|
33
|
+
"
|
|
34
|
+
>
|
|
35
|
+
<span class="icon">>_</span>
|
|
36
|
+
<span class="text">Test Result</span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="panel-actions">
|
|
40
|
+
<button class="icon-toggle" @click="togglePanel">
|
|
41
|
+
{{ isPanelExpanded ? "⌄" : "^" }}
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- ... rest of content ... -->
|
|
47
|
+
|
|
48
|
+
<!-- Testcase Content -->
|
|
49
|
+
<div
|
|
50
|
+
class="panel-content"
|
|
51
|
+
v-if="activeTab === 'testcase' && isPanelExpanded"
|
|
52
|
+
>
|
|
53
|
+
<div class="case-tabs">
|
|
54
|
+
<button
|
|
55
|
+
v-for="(item, index) in testCases"
|
|
56
|
+
:key="item.id"
|
|
57
|
+
class="case-tab-btn"
|
|
58
|
+
:class="{ active: activeTestCaseIndex === index }"
|
|
59
|
+
@click="activeTestCaseIndex = index"
|
|
60
|
+
>
|
|
61
|
+
{{ item.label }}
|
|
62
|
+
<span
|
|
63
|
+
v-if="testCases.length > 1"
|
|
64
|
+
class="case-close-btn"
|
|
65
|
+
@click.stop="deleteTestCase(index)"
|
|
66
|
+
>×</span
|
|
67
|
+
>
|
|
68
|
+
</button>
|
|
69
|
+
<button class="case-tab-btn add" @click="addNewTestCase">+</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="case-inputs">
|
|
73
|
+
<div
|
|
74
|
+
class="input-group"
|
|
75
|
+
v-for="(input, idx) in activeTestCase.inputs"
|
|
76
|
+
:key="idx"
|
|
77
|
+
>
|
|
78
|
+
<label>{{ input.name }}</label>
|
|
79
|
+
<div class="input-box-wrapper">
|
|
80
|
+
<textarea
|
|
81
|
+
class="input-box-edit"
|
|
82
|
+
v-model="input.value"
|
|
83
|
+
rows="1"
|
|
84
|
+
></textarea>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Result Content -->
|
|
91
|
+
<div
|
|
92
|
+
class="panel-content result-view"
|
|
93
|
+
v-show="activeTab === 'result' && isPanelExpanded"
|
|
94
|
+
ref="resultPanel"
|
|
95
|
+
>
|
|
96
|
+
<!-- Loading State -->
|
|
97
|
+
<div class="result-loading" v-if="isRunning">
|
|
98
|
+
<div class="spinner"></div>
|
|
99
|
+
<p>Running Code...</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="result-empty" v-else-if="!runResult">
|
|
103
|
+
<p class="empty-text">Click "Run" to see results</p>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="result-content" v-else>
|
|
107
|
+
<!-- Overall Status Header -->
|
|
108
|
+
<div class="result-header">
|
|
109
|
+
<div
|
|
110
|
+
:class="
|
|
111
|
+
runResult.status === 'success' && allTestsPassed
|
|
112
|
+
? 'success'
|
|
113
|
+
: 'error'
|
|
114
|
+
"
|
|
115
|
+
>
|
|
116
|
+
{{
|
|
117
|
+
runResult.status === "success" && allTestsPassed
|
|
118
|
+
? "Accepted"
|
|
119
|
+
: runResult.error
|
|
120
|
+
? "Runtime Error"
|
|
121
|
+
: "Wrong Answer"
|
|
122
|
+
}}
|
|
123
|
+
</div>
|
|
124
|
+
<div class="result-meta">
|
|
125
|
+
Runtime: {{ Math.round(runResult.executionTime) }} ms
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Compile Error Message if any -->
|
|
130
|
+
<div
|
|
131
|
+
v-if="
|
|
132
|
+
runResult.error &&
|
|
133
|
+
(!runResult.testResults || runResult.testResults.length === 0)
|
|
134
|
+
"
|
|
135
|
+
class="error-box"
|
|
136
|
+
>
|
|
137
|
+
{{ runResult.error }}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Test Result Tabs -->
|
|
141
|
+
<div
|
|
142
|
+
v-if="runResult.testResults && runResult.testResults.length > 0"
|
|
143
|
+
>
|
|
144
|
+
<div class="case-tabs result-tabs">
|
|
145
|
+
<button
|
|
146
|
+
v-for="(res, index) in runResult.testResults"
|
|
147
|
+
:key="res.id"
|
|
148
|
+
class="case-tab-btn"
|
|
149
|
+
:class="[
|
|
150
|
+
{ active: activeResultCaseIndex === index },
|
|
151
|
+
res.status,
|
|
152
|
+
]"
|
|
153
|
+
@click="activeResultCaseIndex = index"
|
|
154
|
+
>
|
|
155
|
+
<span class="status-dot"></span>
|
|
156
|
+
Case {{ index + 1 }}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Selected Case Detail -->
|
|
161
|
+
<div
|
|
162
|
+
class="case-detail"
|
|
163
|
+
v-if="runResult.testResults[activeResultCaseIndex]"
|
|
164
|
+
>
|
|
165
|
+
<!-- Inputs Display -->
|
|
166
|
+
<div
|
|
167
|
+
class="input-group"
|
|
168
|
+
v-for="(val, idx) in runResult.testResults[
|
|
169
|
+
activeResultCaseIndex
|
|
170
|
+
].input"
|
|
171
|
+
:key="idx"
|
|
172
|
+
>
|
|
173
|
+
<label
|
|
174
|
+
v-if="
|
|
175
|
+
testCases[activeResultCaseIndex] &&
|
|
176
|
+
testCases[activeResultCaseIndex].inputs[idx]
|
|
177
|
+
"
|
|
178
|
+
>
|
|
179
|
+
{{ testCases[activeResultCaseIndex].inputs[idx].name }}
|
|
180
|
+
</label>
|
|
181
|
+
<label v-else>Input {{ idx + 1 }}</label>
|
|
182
|
+
<div class="code-block input-block">
|
|
183
|
+
{{ JSON.stringify(val) }}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div
|
|
188
|
+
class="input-group"
|
|
189
|
+
v-if="runResult.testResults[activeResultCaseIndex].log"
|
|
190
|
+
>
|
|
191
|
+
<label>Stdout</label>
|
|
192
|
+
<div class="code-block log">
|
|
193
|
+
{{ runResult.testResults[activeResultCaseIndex].log }}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="input-group">
|
|
198
|
+
<label>Output</label>
|
|
199
|
+
<div
|
|
200
|
+
class="code-block"
|
|
201
|
+
:class="{
|
|
202
|
+
error:
|
|
203
|
+
runResult.testResults[activeResultCaseIndex].status !==
|
|
204
|
+
'passed',
|
|
205
|
+
}"
|
|
206
|
+
>
|
|
207
|
+
{{
|
|
208
|
+
runResult.testResults[activeResultCaseIndex].error
|
|
209
|
+
? runResult.testResults[activeResultCaseIndex].error
|
|
210
|
+
: JSON.stringify(
|
|
211
|
+
runResult.testResults[activeResultCaseIndex].output,
|
|
212
|
+
)
|
|
213
|
+
}}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="input-group">
|
|
218
|
+
<label>Expected</label>
|
|
219
|
+
<div class="code-block success">
|
|
220
|
+
{{
|
|
221
|
+
runResult.testResults[activeResultCaseIndex].expected ===
|
|
222
|
+
undefined
|
|
223
|
+
? "N/A"
|
|
224
|
+
: JSON.stringify(
|
|
225
|
+
runResult.testResults[activeResultCaseIndex]
|
|
226
|
+
.expected,
|
|
227
|
+
)
|
|
228
|
+
}}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</template>
|
|
239
|
+
|
|
240
|
+
<script lang="ts">
|
|
241
|
+
import {
|
|
242
|
+
defineComponent,
|
|
243
|
+
ref,
|
|
244
|
+
computed,
|
|
245
|
+
onMounted,
|
|
246
|
+
onBeforeUnmount,
|
|
247
|
+
watch,
|
|
248
|
+
type PropType,
|
|
249
|
+
} from "vue";
|
|
250
|
+
import { SmartCodeEditor } from "../../core/SmartCodeEditor";
|
|
251
|
+
import type { SmartCodeEditorOptions, RunResult } from "../../types";
|
|
252
|
+
|
|
253
|
+
export default defineComponent({
|
|
254
|
+
name: "SmartCodeEditor",
|
|
255
|
+
props: {
|
|
256
|
+
modelValue: {
|
|
257
|
+
type: String,
|
|
258
|
+
default: "",
|
|
259
|
+
},
|
|
260
|
+
language: {
|
|
261
|
+
type: String,
|
|
262
|
+
default: "javascript",
|
|
263
|
+
},
|
|
264
|
+
theme: {
|
|
265
|
+
type: String,
|
|
266
|
+
default: "vs",
|
|
267
|
+
},
|
|
268
|
+
config: {
|
|
269
|
+
type: Object as PropType<SmartCodeEditorOptions>,
|
|
270
|
+
default: undefined,
|
|
271
|
+
},
|
|
272
|
+
questionContent: {
|
|
273
|
+
type: String,
|
|
274
|
+
default: "",
|
|
275
|
+
},
|
|
276
|
+
questionMarkdown: {
|
|
277
|
+
type: Object as () => import("../../types/question").QuestionMarkdownOptions,
|
|
278
|
+
default: undefined,
|
|
279
|
+
},
|
|
280
|
+
showQuestionPanel: {
|
|
281
|
+
type: Boolean,
|
|
282
|
+
default: true,
|
|
283
|
+
},
|
|
284
|
+
enableRun: {
|
|
285
|
+
type: Boolean,
|
|
286
|
+
default: true,
|
|
287
|
+
},
|
|
288
|
+
enableLanguageSwitch: {
|
|
289
|
+
type: Boolean,
|
|
290
|
+
default: true,
|
|
291
|
+
},
|
|
292
|
+
readOnly: {
|
|
293
|
+
type: Boolean,
|
|
294
|
+
default: false,
|
|
295
|
+
},
|
|
296
|
+
runTimeout: {
|
|
297
|
+
type: Number,
|
|
298
|
+
default: 5000,
|
|
299
|
+
},
|
|
300
|
+
disableLocalRun: {
|
|
301
|
+
type: Boolean,
|
|
302
|
+
default: false,
|
|
303
|
+
},
|
|
304
|
+
customRunner: {
|
|
305
|
+
type: Function as PropType<SmartCodeEditorOptions["customRunner"]>,
|
|
306
|
+
default: undefined,
|
|
307
|
+
},
|
|
308
|
+
customSubmitRunner: {
|
|
309
|
+
type: Function as PropType<
|
|
310
|
+
(
|
|
311
|
+
code: string,
|
|
312
|
+
language: string,
|
|
313
|
+
testCases: any,
|
|
314
|
+
silent: boolean,
|
|
315
|
+
) => Promise<RunResult>
|
|
316
|
+
>,
|
|
317
|
+
default: undefined,
|
|
318
|
+
},
|
|
319
|
+
enableSubmit: {
|
|
320
|
+
type: Boolean,
|
|
321
|
+
default: true,
|
|
322
|
+
},
|
|
323
|
+
testCases: {
|
|
324
|
+
type: Array as () => import("../../types").UITestCase[],
|
|
325
|
+
default: () => [],
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// ===== 新增:题目配置相关 Props =====
|
|
329
|
+
|
|
330
|
+
/** 题目完整配置(可选) */
|
|
331
|
+
questionConfig: {
|
|
332
|
+
type: Object as () => import("../../types/question").QuestionConfig,
|
|
333
|
+
default: undefined,
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
/** 自定义语言模板映射(可选,会覆盖 questionConfig) */
|
|
337
|
+
languageTemplates: {
|
|
338
|
+
type: Object as () => import("../../types/question").LanguageTemplates,
|
|
339
|
+
default: undefined,
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/** 支持的语言列表(可选,限制可用语言) */
|
|
343
|
+
supportedLanguages: {
|
|
344
|
+
type: Array as () => string[],
|
|
345
|
+
default: undefined,
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/** 统一题目配置对象 (推荐) */
|
|
349
|
+
question: {
|
|
350
|
+
type: Object as () => import("../../types/question").QuestionConfig,
|
|
351
|
+
default: undefined,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
emits: ["update:modelValue", "run", "submit", "language-change"],
|
|
355
|
+
setup(props, { emit }) {
|
|
356
|
+
const wrapperRef = ref<HTMLElement>();
|
|
357
|
+
const containerRef = ref<HTMLElement>();
|
|
358
|
+
const terminalPanelRef = ref<HTMLElement>();
|
|
359
|
+
let editorInstance: SmartCodeEditor | null = null;
|
|
360
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
361
|
+
|
|
362
|
+
// Terminal panel state
|
|
363
|
+
const activeTab = ref<"testcase" | "result">("testcase");
|
|
364
|
+
const isPanelExpanded = ref(true);
|
|
365
|
+
const activeTestCaseIndex = ref(0);
|
|
366
|
+
const activeResultCaseIndex = ref(0); // 新增:结果面板选中的 Case 索引
|
|
367
|
+
const runResult = ref<RunResult | null>(null);
|
|
368
|
+
const isRunning = ref(false); // 新增:运行状态
|
|
369
|
+
const allTestsPassed = computed(() => {
|
|
370
|
+
if (!runResult.value || !runResult.value.testResults) return false;
|
|
371
|
+
return (
|
|
372
|
+
runResult.value.testResults.length > 0 &&
|
|
373
|
+
runResult.value.testResults.every((t) => t.status === "passed")
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
const panelHeight = ref(240);
|
|
377
|
+
let isResizing = false;
|
|
378
|
+
let startY = 0;
|
|
379
|
+
let startHeight = 0;
|
|
380
|
+
|
|
381
|
+
// Use prop if available, otherwise default to empty or specific logic.
|
|
382
|
+
// However, since we want to support the user case of "Instructors entering questions",
|
|
383
|
+
// we should validly initialize it.
|
|
384
|
+
// Here we initialize from props, or empty array if not provided.
|
|
385
|
+
// The parent (dev-app) is responsible for providing the initial cases.
|
|
386
|
+
const testCases = ref<import("../../types").UITestCase[]>(
|
|
387
|
+
props.testCases && props.testCases.length > 0
|
|
388
|
+
? JSON.parse(JSON.stringify(props.testCases)) // Deep copy to avoid mutating prop
|
|
389
|
+
: props.question && props.question.testCases
|
|
390
|
+
? JSON.parse(JSON.stringify(props.question.testCases))
|
|
391
|
+
: [],
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const getMergedQuestionMarkdown = () => {
|
|
395
|
+
const base = props.questionMarkdown || props.config?.questionMarkdown;
|
|
396
|
+
if (!base) return undefined;
|
|
397
|
+
const value =
|
|
398
|
+
base.value ??
|
|
399
|
+
(props.question as any)?.descriptionMarkdown ??
|
|
400
|
+
props.question?.description ??
|
|
401
|
+
"";
|
|
402
|
+
return { ...base, value };
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// If empty default needed for independent testing when no prop provided:
|
|
406
|
+
if (testCases.value.length === 0) {
|
|
407
|
+
testCases.value = [
|
|
408
|
+
{
|
|
409
|
+
id: 1,
|
|
410
|
+
label: "Case 1",
|
|
411
|
+
inputs: [],
|
|
412
|
+
expected: "",
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Watch prop changes to update local state if parent updates it
|
|
418
|
+
watch(
|
|
419
|
+
() => props.testCases,
|
|
420
|
+
(newVal) => {
|
|
421
|
+
if (newVal) {
|
|
422
|
+
testCases.value = JSON.parse(JSON.stringify(newVal));
|
|
423
|
+
activeTestCaseIndex.value = 0;
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
{ deep: true },
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const activeTestCase = computed(
|
|
430
|
+
() => testCases.value[activeTestCaseIndex.value],
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const togglePanel = () => {
|
|
434
|
+
isPanelExpanded.value = !isPanelExpanded.value;
|
|
435
|
+
// Reset height when expanding if it was collapsed or too small
|
|
436
|
+
if (isPanelExpanded.value && panelHeight.value < 100) {
|
|
437
|
+
panelHeight.value = 240;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const startResize = (e: MouseEvent) => {
|
|
442
|
+
isResizing = true;
|
|
443
|
+
startY = e.clientY;
|
|
444
|
+
startHeight = panelHeight.value;
|
|
445
|
+
if (!isPanelExpanded.value) {
|
|
446
|
+
isPanelExpanded.value = true;
|
|
447
|
+
}
|
|
448
|
+
document.addEventListener("mousemove", doResize);
|
|
449
|
+
document.addEventListener("mouseup", stopResize);
|
|
450
|
+
document.body.style.cursor = "ns-resize";
|
|
451
|
+
document.body.style.userSelect = "none";
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const doResize = (e: MouseEvent) => {
|
|
455
|
+
if (!isResizing) return;
|
|
456
|
+
e.preventDefault(); // Prevent text selection while dragging
|
|
457
|
+
|
|
458
|
+
requestAnimationFrame(() => {
|
|
459
|
+
const dy = startY - e.clientY;
|
|
460
|
+
const newHeight = startHeight + dy;
|
|
461
|
+
|
|
462
|
+
// Min height 100px, max height 80% of window
|
|
463
|
+
const maxHeight = window.innerHeight * 0.8;
|
|
464
|
+
if (newHeight >= 100 && newHeight <= maxHeight) {
|
|
465
|
+
panelHeight.value = newHeight;
|
|
466
|
+
// Trigger layout update during resize for smoothness
|
|
467
|
+
editorInstance?.layout();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const stopResize = () => {
|
|
473
|
+
isResizing = false;
|
|
474
|
+
document.removeEventListener("mousemove", doResize);
|
|
475
|
+
document.removeEventListener("mouseup", stopResize);
|
|
476
|
+
document.body.style.cursor = "";
|
|
477
|
+
document.body.style.userSelect = "";
|
|
478
|
+
// Ensure editor resizes after panel resize stops
|
|
479
|
+
editorInstance?.layout();
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const addNewTestCase = () => {
|
|
483
|
+
const newId = testCases.value.length + 1;
|
|
484
|
+
|
|
485
|
+
// Clone inputs structure from the first test case if available
|
|
486
|
+
let newInputs = [
|
|
487
|
+
{ name: "nums =", value: "[]" },
|
|
488
|
+
{ name: "target =", value: "0" },
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
if (testCases.value.length > 0) {
|
|
492
|
+
newInputs = testCases.value[0].inputs.map((input) => ({
|
|
493
|
+
name: input.name,
|
|
494
|
+
value: "", // Empty value for new case
|
|
495
|
+
}));
|
|
496
|
+
} else if (
|
|
497
|
+
props.question &&
|
|
498
|
+
props.question.functionStructure &&
|
|
499
|
+
props.question.functionStructure.args
|
|
500
|
+
) {
|
|
501
|
+
// Fallback: derive from question config
|
|
502
|
+
newInputs = props.question.functionStructure.args.map((arg) => ({
|
|
503
|
+
name: `${arg.name} =`,
|
|
504
|
+
value: "",
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
testCases.value.push({
|
|
509
|
+
id: newId,
|
|
510
|
+
label: `Case ${newId}`,
|
|
511
|
+
inputs: newInputs,
|
|
512
|
+
expected: "",
|
|
513
|
+
});
|
|
514
|
+
activeTestCaseIndex.value = testCases.value.length - 1;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const deleteTestCase = (index: number) => {
|
|
518
|
+
if (testCases.value.length <= 1) return;
|
|
519
|
+
|
|
520
|
+
testCases.value.splice(index, 1);
|
|
521
|
+
|
|
522
|
+
if (activeTestCaseIndex.value >= testCases.value.length) {
|
|
523
|
+
activeTestCaseIndex.value = testCases.value.length - 1;
|
|
524
|
+
} else if (activeTestCaseIndex.value > index) {
|
|
525
|
+
activeTestCaseIndex.value--;
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const parseValue = (val: string) => {
|
|
530
|
+
try {
|
|
531
|
+
// 简单处理:如果是数字字符串,转数字;如果是 JSON 格式,解析 JSON
|
|
532
|
+
if (!isNaN(Number(val)) && val.trim() !== "") return Number(val);
|
|
533
|
+
return JSON.parse(val);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
return val; // 无法解析则作为字符串
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// 将 UI 用例转换为运行时用例
|
|
540
|
+
const getRuntimeTestCases = () => {
|
|
541
|
+
return testCases.value.map((tc) => ({
|
|
542
|
+
id: tc.id,
|
|
543
|
+
input: tc.inputs.map((i) => parseValue(i.value)),
|
|
544
|
+
expected: parseValue(tc.expected),
|
|
545
|
+
}));
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// 同步测试用例到编辑器实例
|
|
549
|
+
const syncTestCasesToEditor = () => {
|
|
550
|
+
if (editorInstance) {
|
|
551
|
+
editorInstance.setTestCases(getRuntimeTestCases());
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// 监听测试用例变化,同步到编辑器
|
|
556
|
+
watch(
|
|
557
|
+
testCases,
|
|
558
|
+
() => {
|
|
559
|
+
syncTestCasesToEditor();
|
|
560
|
+
},
|
|
561
|
+
{ deep: true },
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// 运行完成的回调
|
|
565
|
+
const handleRunComplete = (result: RunResult) => {
|
|
566
|
+
isRunning.value = false;
|
|
567
|
+
runResult.value = result;
|
|
568
|
+
activeTab.value = "result";
|
|
569
|
+
isPanelExpanded.value = true;
|
|
570
|
+
|
|
571
|
+
// 默认选中第一个 failed 的 case,或者第一个 case
|
|
572
|
+
if (result.testResults && result.testResults.length > 0) {
|
|
573
|
+
const firstFailed = result.testResults.findIndex(
|
|
574
|
+
(r) => r.status !== "passed",
|
|
575
|
+
);
|
|
576
|
+
activeResultCaseIndex.value = firstFailed >= 0 ? firstFailed : 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
emit("run", result);
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const handleRun = async () => {
|
|
583
|
+
// 切换到结果标签页并显示 loading
|
|
584
|
+
activeTab.value = "result";
|
|
585
|
+
isPanelExpanded.value = true;
|
|
586
|
+
isRunning.value = true;
|
|
587
|
+
runResult.value = null; // 清除旧结果
|
|
588
|
+
|
|
589
|
+
// 调用编辑器运行
|
|
590
|
+
// 注意:这里是一个异步调用,但我们通过回调 handleRunComplete 来处理完成状态
|
|
591
|
+
// 这里 await 如果 CodeRunner 内部抛错也能捕获
|
|
592
|
+
try {
|
|
593
|
+
const result = await editorInstance?.run();
|
|
594
|
+
// If disableLocalRun is true, the result will be a placeholder
|
|
595
|
+
// The actual result will come from external handler via prop callback
|
|
596
|
+
// But we still need to wait for it somehow...
|
|
597
|
+
if (result && result.status !== "success") {
|
|
598
|
+
// If we got an error from the internal runner disable logic, handle it
|
|
599
|
+
isRunning.value = false;
|
|
600
|
+
}
|
|
601
|
+
} catch (e) {
|
|
602
|
+
isRunning.value = false;
|
|
603
|
+
console.error("Run failed:", e);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
onMounted(() => {
|
|
608
|
+
if (!containerRef.value || !terminalPanelRef.value) return;
|
|
609
|
+
|
|
610
|
+
const mergedQuestionMarkdown = getMergedQuestionMarkdown();
|
|
611
|
+
|
|
612
|
+
const options: SmartCodeEditorOptions = {
|
|
613
|
+
...(props.config || {}),
|
|
614
|
+
container: containerRef.value,
|
|
615
|
+
language: props.language,
|
|
616
|
+
theme: props.theme,
|
|
617
|
+
value: props.modelValue,
|
|
618
|
+
questionContent: mergedQuestionMarkdown
|
|
619
|
+
? ""
|
|
620
|
+
: props.questionContent || props.question?.description,
|
|
621
|
+
questionMarkdown: mergedQuestionMarkdown,
|
|
622
|
+
showQuestionPanel: props.showQuestionPanel,
|
|
623
|
+
enableRun: props.enableRun,
|
|
624
|
+
enableSubmit: props.enableSubmit,
|
|
625
|
+
enableLanguageSwitch: props.enableLanguageSwitch,
|
|
626
|
+
readOnly: props.readOnly,
|
|
627
|
+
runTimeout: props.runTimeout,
|
|
628
|
+
disableLocalRun: props.disableLocalRun,
|
|
629
|
+
customRunner: props.customRunner,
|
|
630
|
+
|
|
631
|
+
// 新增:题目配置相关
|
|
632
|
+
questionConfig: props.questionConfig || props.question,
|
|
633
|
+
languageTemplates:
|
|
634
|
+
props.languageTemplates || props.question?.languageTemplates,
|
|
635
|
+
supportedLanguages:
|
|
636
|
+
props.supportedLanguages || props.question?.supportedLanguages,
|
|
637
|
+
|
|
638
|
+
onChange: (value: string) => {
|
|
639
|
+
emit("update:modelValue", value);
|
|
640
|
+
},
|
|
641
|
+
onRun: handleRunComplete,
|
|
642
|
+
onSubmit: async (code: string, language: string) => {
|
|
643
|
+
if (props.customSubmitRunner) {
|
|
644
|
+
// Show result panel
|
|
645
|
+
activeTab.value = "result";
|
|
646
|
+
isPanelExpanded.value = true;
|
|
647
|
+
isRunning.value = true;
|
|
648
|
+
runResult.value = null;
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
// Call custom submit runner (wrapper should handle args)
|
|
652
|
+
// We pass null for testCases because backend uses hidden cases
|
|
653
|
+
const result = await props.customSubmitRunner(
|
|
654
|
+
code,
|
|
655
|
+
language,
|
|
656
|
+
null,
|
|
657
|
+
true,
|
|
658
|
+
);
|
|
659
|
+
handleRunComplete(result);
|
|
660
|
+
} catch (e) {
|
|
661
|
+
console.error("Submit failed:", e);
|
|
662
|
+
isRunning.value = false;
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
emit("submit", {
|
|
666
|
+
code,
|
|
667
|
+
language,
|
|
668
|
+
testCases: getRuntimeTestCases(),
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
onLanguageChange: (language: string) => {
|
|
673
|
+
emit("language-change", language);
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
editorInstance = new SmartCodeEditor(options);
|
|
678
|
+
|
|
679
|
+
// 初始化同步测试用例
|
|
680
|
+
syncTestCasesToEditor();
|
|
681
|
+
|
|
682
|
+
// 如果在初始化时就有 question 数据,确保代码正确加载
|
|
683
|
+
if (props.question && props.question.languageTemplates) {
|
|
684
|
+
const currentLang = editorInstance.getLanguage();
|
|
685
|
+
const template = props.question.languageTemplates[currentLang];
|
|
686
|
+
|
|
687
|
+
console.log("🔧 初始化时检查代码:", {
|
|
688
|
+
hasQuestion: !!props.question,
|
|
689
|
+
currentLang,
|
|
690
|
+
hasTemplate: !!template,
|
|
691
|
+
modelValue: props.modelValue?.substring(0, 50),
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// 如果 modelValue 是空的或未定义,但有题目模板,使用题目模板
|
|
695
|
+
if (!props.modelValue && template) {
|
|
696
|
+
console.log("⚠️ 模板未正确传递,手动设置");
|
|
697
|
+
editorInstance.setValue(template);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Move terminal-panel into sce-right-container (as sibling of sce-editor-panel)
|
|
702
|
+
setTimeout(() => {
|
|
703
|
+
const rightContainer = containerRef.value?.querySelector(
|
|
704
|
+
".sce-right-container",
|
|
705
|
+
);
|
|
706
|
+
if (rightContainer && terminalPanelRef.value) {
|
|
707
|
+
rightContainer.appendChild(terminalPanelRef.value);
|
|
708
|
+
}
|
|
709
|
+
}, 0);
|
|
710
|
+
|
|
711
|
+
// 监听容器大小变化
|
|
712
|
+
if (wrapperRef.value) {
|
|
713
|
+
resizeObserver = new ResizeObserver(() => {
|
|
714
|
+
editorInstance?.resize();
|
|
715
|
+
});
|
|
716
|
+
resizeObserver.observe(wrapperRef.value);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// 监听 props 变化并同步到编辑器
|
|
721
|
+
watch(
|
|
722
|
+
() => props.language,
|
|
723
|
+
(newLang) => {
|
|
724
|
+
editorInstance?.setLanguage(newLang);
|
|
725
|
+
runResult.value = null;
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
watch(
|
|
730
|
+
() => props.theme,
|
|
731
|
+
(newTheme) => {
|
|
732
|
+
editorInstance?.setTheme(newTheme);
|
|
733
|
+
},
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
watch(
|
|
737
|
+
() => props.modelValue,
|
|
738
|
+
(newValue) => {
|
|
739
|
+
if (editorInstance && editorInstance.getValue() !== newValue) {
|
|
740
|
+
editorInstance.setValue(newValue);
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
watch(
|
|
746
|
+
() => props.questionContent,
|
|
747
|
+
(newContent) => {
|
|
748
|
+
if (getMergedQuestionMarkdown()) return;
|
|
749
|
+
editorInstance?.setQuestionContent(newContent);
|
|
750
|
+
},
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
watch(
|
|
754
|
+
() => [props.questionMarkdown, props.config, props.question],
|
|
755
|
+
() => {
|
|
756
|
+
const merged = getMergedQuestionMarkdown();
|
|
757
|
+
if (!merged) return;
|
|
758
|
+
editorInstance?.setQuestionMarkdown(merged.value || "", merged);
|
|
759
|
+
},
|
|
760
|
+
{ deep: true },
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
watch(
|
|
764
|
+
() => props.question,
|
|
765
|
+
(newQ) => {
|
|
766
|
+
if (!newQ) return;
|
|
767
|
+
|
|
768
|
+
// Update Question Content if prop not set
|
|
769
|
+
if (
|
|
770
|
+
!props.questionContent &&
|
|
771
|
+
!getMergedQuestionMarkdown() &&
|
|
772
|
+
newQ.description
|
|
773
|
+
) {
|
|
774
|
+
editorInstance?.setQuestionContent(newQ.description);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Update Test Cases if prop not set
|
|
778
|
+
if (
|
|
779
|
+
(!props.testCases || props.testCases.length === 0) &&
|
|
780
|
+
newQ.testCases
|
|
781
|
+
) {
|
|
782
|
+
testCases.value = JSON.parse(JSON.stringify(newQ.testCases));
|
|
783
|
+
activeTestCaseIndex.value = 0;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Update config for languages and templates
|
|
787
|
+
editorInstance?.updateConfig({
|
|
788
|
+
questionConfig: newQ,
|
|
789
|
+
languageTemplates: newQ.languageTemplates,
|
|
790
|
+
supportedLanguages: newQ.supportedLanguages,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// Update code if editor is empty or using fallback template
|
|
794
|
+
if (editorInstance && newQ.defaultLanguage && newQ.languageTemplates) {
|
|
795
|
+
const currentCode = editorInstance.getValue();
|
|
796
|
+
const currentLang = editorInstance.getLanguage();
|
|
797
|
+
|
|
798
|
+
// 如果代码是空的,或者当前语言与题目默认语言一致,则加载题目模板
|
|
799
|
+
if (!currentCode || currentLang === newQ.defaultLanguage) {
|
|
800
|
+
const template = newQ.languageTemplates[currentLang];
|
|
801
|
+
if (template) {
|
|
802
|
+
editorInstance.setValue(template);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
{ deep: true },
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
watch(
|
|
811
|
+
() => props.showQuestionPanel,
|
|
812
|
+
(show) => {
|
|
813
|
+
if (show) {
|
|
814
|
+
editorInstance?.showQuestionPanel();
|
|
815
|
+
} else {
|
|
816
|
+
editorInstance?.hideQuestionPanel();
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
onBeforeUnmount(() => {
|
|
822
|
+
resizeObserver?.disconnect();
|
|
823
|
+
editorInstance?.destroy();
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
wrapperRef,
|
|
828
|
+
containerRef,
|
|
829
|
+
terminalPanelRef,
|
|
830
|
+
activeTab,
|
|
831
|
+
isPanelExpanded,
|
|
832
|
+
activeTestCaseIndex,
|
|
833
|
+
testCases,
|
|
834
|
+
activeTestCase,
|
|
835
|
+
runResult,
|
|
836
|
+
panelHeight,
|
|
837
|
+
startResize,
|
|
838
|
+
togglePanel,
|
|
839
|
+
addNewTestCase,
|
|
840
|
+
deleteTestCase,
|
|
841
|
+
activeResultCaseIndex,
|
|
842
|
+
isRunning,
|
|
843
|
+
allTestsPassed,
|
|
844
|
+
};
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
</script>
|
|
848
|
+
|
|
849
|
+
<style scoped>
|
|
850
|
+
.sce-wrapper {
|
|
851
|
+
width: 100%;
|
|
852
|
+
height: 100%;
|
|
853
|
+
overflow: hidden;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.sce-editor-area {
|
|
857
|
+
width: 100%;
|
|
858
|
+
height: 100%;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/* Terminal Panel - will be moved into sce-right-container */
|
|
862
|
+
.terminal-panel {
|
|
863
|
+
flex-shrink: 0;
|
|
864
|
+
/* height set via inline style for resizing */
|
|
865
|
+
background: var(--panel-bg, #1e1e1e);
|
|
866
|
+
border-top: 1px solid var(--border-color, #333);
|
|
867
|
+
display: flex;
|
|
868
|
+
flex-direction: column;
|
|
869
|
+
transition: height 0.3s ease;
|
|
870
|
+
position: relative;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.terminal-panel.collapsed {
|
|
874
|
+
height: 40px !important;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.resize-handle {
|
|
878
|
+
position: absolute;
|
|
879
|
+
top: -4px;
|
|
880
|
+
left: 0;
|
|
881
|
+
right: 0;
|
|
882
|
+
height: 8px;
|
|
883
|
+
cursor: ns-resize;
|
|
884
|
+
z-index: 10;
|
|
885
|
+
background: transparent;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.resize-handle:hover {
|
|
889
|
+
background: rgba(255, 255, 255, 0.1);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.panel-tabs {
|
|
893
|
+
display: flex;
|
|
894
|
+
align-items: center;
|
|
895
|
+
height: 40px;
|
|
896
|
+
background: var(--header-bg, #252526);
|
|
897
|
+
border-bottom: 1px solid var(--border-color, #333);
|
|
898
|
+
padding: 0 8px;
|
|
899
|
+
gap: 16px;
|
|
900
|
+
flex-shrink: 0;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.tab-item {
|
|
904
|
+
display: flex;
|
|
905
|
+
align-items: center;
|
|
906
|
+
gap: 6px;
|
|
907
|
+
cursor: pointer;
|
|
908
|
+
padding: 6px 12px;
|
|
909
|
+
border-radius: 4px;
|
|
910
|
+
font-size: 13px;
|
|
911
|
+
color: var(--text-color, #cccccc);
|
|
912
|
+
opacity: 0.7;
|
|
913
|
+
transition: opacity 0.2s;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.tab-item:hover {
|
|
917
|
+
opacity: 1;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.tab-item.active {
|
|
921
|
+
opacity: 1;
|
|
922
|
+
font-weight: 500;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.tab-item .icon {
|
|
926
|
+
font-size: 14px;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.panel-actions {
|
|
930
|
+
margin-left: auto;
|
|
931
|
+
display: flex;
|
|
932
|
+
gap: 8px;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.icon-toggle {
|
|
936
|
+
background: none;
|
|
937
|
+
border: none;
|
|
938
|
+
color: var(--text-color, #cccccc);
|
|
939
|
+
cursor: pointer;
|
|
940
|
+
font-size: 14px;
|
|
941
|
+
opacity: 0.6;
|
|
942
|
+
transition: opacity 0.2s;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
.icon-toggle:hover {
|
|
946
|
+
opacity: 1;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.panel-content {
|
|
950
|
+
flex: 1;
|
|
951
|
+
padding: 16px;
|
|
952
|
+
overflow-y: auto;
|
|
953
|
+
font-family: -apple-system, sans-serif;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.case-tabs {
|
|
957
|
+
display: flex;
|
|
958
|
+
gap: 8px;
|
|
959
|
+
margin-bottom: 16px;
|
|
960
|
+
flex-wrap: wrap;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.case-tab-btn {
|
|
964
|
+
background: transparent;
|
|
965
|
+
border: none;
|
|
966
|
+
color: var(--text-color, #cccccc);
|
|
967
|
+
padding: 6px 10px;
|
|
968
|
+
border-radius: 16px;
|
|
969
|
+
font-size: 12px;
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
transition: all 0.2s;
|
|
972
|
+
opacity: 0.8;
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: center;
|
|
975
|
+
gap: 6px;
|
|
976
|
+
flex-shrink: 0;
|
|
977
|
+
position: relative;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.case-tab-btn:hover {
|
|
981
|
+
background: var(--hover-bg, #3c3c3c);
|
|
982
|
+
opacity: 1;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.case-tab-btn.active {
|
|
986
|
+
background: var(--hover-bg, #3c3c3c);
|
|
987
|
+
/* color: white; */
|
|
988
|
+
font-weight: 500;
|
|
989
|
+
opacity: 1;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.case-tab-btn.add {
|
|
993
|
+
padding: 6px 10px;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.case-close-btn {
|
|
997
|
+
position: absolute;
|
|
998
|
+
top: -4px;
|
|
999
|
+
right: -4px;
|
|
1000
|
+
width: 16px;
|
|
1001
|
+
height: 16px;
|
|
1002
|
+
display: none;
|
|
1003
|
+
align-items: center;
|
|
1004
|
+
justify-content: center;
|
|
1005
|
+
font-size: 16px;
|
|
1006
|
+
line-height: 1;
|
|
1007
|
+
opacity: 1;
|
|
1008
|
+
transition: transform 0.2s;
|
|
1009
|
+
transform: scale(1);
|
|
1010
|
+
border-radius: 50%;
|
|
1011
|
+
background: rgba(128, 128, 128, 0.5);
|
|
1012
|
+
color: #fff;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.case-tab-btn:hover .case-close-btn {
|
|
1016
|
+
display: flex;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.case-close-btn:hover {
|
|
1020
|
+
opacity: 1;
|
|
1021
|
+
background: #f44336;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.case-inputs .input-group {
|
|
1025
|
+
margin-bottom: 16px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.case-inputs label {
|
|
1029
|
+
display: block;
|
|
1030
|
+
font-size: 12px;
|
|
1031
|
+
color: var(--text-color, #cccccc);
|
|
1032
|
+
margin-bottom: 6px;
|
|
1033
|
+
opacity: 0.8;
|
|
1034
|
+
font-family: monospace;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.input-box-wrapper {
|
|
1038
|
+
background: var(--header-bg, #252526);
|
|
1039
|
+
border: 1px solid var(--border-color, #333);
|
|
1040
|
+
border-radius: 6px;
|
|
1041
|
+
padding: 4px;
|
|
1042
|
+
font-size: 0;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.input-box-edit {
|
|
1046
|
+
width: 100%;
|
|
1047
|
+
background: transparent;
|
|
1048
|
+
border: none;
|
|
1049
|
+
color: var(--text-color, #cccccc);
|
|
1050
|
+
font-family: monospace;
|
|
1051
|
+
font-size: 13px;
|
|
1052
|
+
resize: none;
|
|
1053
|
+
outline: none;
|
|
1054
|
+
padding: 4px 8px;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.input-box {
|
|
1058
|
+
background: var(--header-bg, #252526);
|
|
1059
|
+
border: 1px solid var(--border-color, #333);
|
|
1060
|
+
border-radius: 6px;
|
|
1061
|
+
padding: 8px 12px;
|
|
1062
|
+
font-family: monospace;
|
|
1063
|
+
font-size: 13px;
|
|
1064
|
+
color: var(--text-color, #cccccc);
|
|
1065
|
+
white-space: pre-wrap;
|
|
1066
|
+
word-break: break-word;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.result-header {
|
|
1070
|
+
display: flex;
|
|
1071
|
+
align-items: center;
|
|
1072
|
+
gap: 12px;
|
|
1073
|
+
margin-bottom: 16px;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.result-title {
|
|
1077
|
+
font-size: 18px;
|
|
1078
|
+
font-weight: 600;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.result-title.success {
|
|
1082
|
+
color: #4caf50;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.result-title.error {
|
|
1086
|
+
color: #ef5350;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.result-meta {
|
|
1090
|
+
font-size: 13px;
|
|
1091
|
+
color: var(--text-color);
|
|
1092
|
+
opacity: 0.6;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.result-tabs {
|
|
1096
|
+
margin-bottom: 20px;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.case-tab-btn.passed .status-dot {
|
|
1100
|
+
background: #4caf50;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.case-tab-btn.failed .status-dot {
|
|
1104
|
+
background: #ef5350;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.case-tab-btn.error .status-dot {
|
|
1108
|
+
background: #ef5350;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.status-dot {
|
|
1112
|
+
width: 6px;
|
|
1113
|
+
height: 6px;
|
|
1114
|
+
border-radius: 50%;
|
|
1115
|
+
background: #ccc;
|
|
1116
|
+
display: inline-block;
|
|
1117
|
+
margin-right: 4px;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/* Override default active style for result tabs */
|
|
1121
|
+
.result-tabs .case-tab-btn {
|
|
1122
|
+
background: var(--hover-bg);
|
|
1123
|
+
color: var(--text-color);
|
|
1124
|
+
}
|
|
1125
|
+
.result-tabs .case-tab-btn.active {
|
|
1126
|
+
background: var(--active-bg);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.input-group label {
|
|
1130
|
+
margin-bottom: 8px;
|
|
1131
|
+
display: block;
|
|
1132
|
+
color: var(--text-color);
|
|
1133
|
+
opacity: 0.7;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.code-block {
|
|
1137
|
+
background: var(--header-bg);
|
|
1138
|
+
border: 1px solid var(--border-color);
|
|
1139
|
+
border-radius: 6px;
|
|
1140
|
+
padding: 12px;
|
|
1141
|
+
font-family: monospace;
|
|
1142
|
+
font-size: 13px;
|
|
1143
|
+
color: var(--text-color);
|
|
1144
|
+
white-space: pre-wrap;
|
|
1145
|
+
word-break: break-work;
|
|
1146
|
+
margin-bottom: 16px;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.code-block.success {
|
|
1150
|
+
/* 弱绿色背景? 根据 mock design,其实是普通的灰色背景,只是文字颜色可能不同? */
|
|
1151
|
+
/* Mockup shows text is green for Expected */
|
|
1152
|
+
color: #4caf50;
|
|
1153
|
+
font-weight: 500;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.code-block.error {
|
|
1157
|
+
color: #ef5350;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.param-line {
|
|
1161
|
+
margin-bottom: 4px;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.param-name {
|
|
1165
|
+
color: var(--text-color);
|
|
1166
|
+
opacity: 0.6;
|
|
1167
|
+
margin-right: 8px;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.param-value {
|
|
1171
|
+
font-weight: 500;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.error-box {
|
|
1175
|
+
background: rgba(239, 83, 80, 0.1);
|
|
1176
|
+
border: 1px solid #ef5350;
|
|
1177
|
+
color: #ef5350;
|
|
1178
|
+
padding: 12px;
|
|
1179
|
+
border-radius: 6px;
|
|
1180
|
+
margin-bottom: 16px;
|
|
1181
|
+
white-space: pre-wrap;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.empty-state {
|
|
1185
|
+
color: var(--text-color, #cccccc);
|
|
1186
|
+
opacity: 0.5;
|
|
1187
|
+
margin-top: 20px;
|
|
1188
|
+
text-align: center;
|
|
1189
|
+
}
|
|
1190
|
+
</style>
|