vite-plugin-automock 1.0.3 → 1.1.1

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.
@@ -0,0 +1,1951 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="UTF-8" />
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <title>Mock Inspector</title>
6
+ <style>
7
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
8
+
9
+ :root {
10
+ --bg-primary: #ffffff;
11
+ --bg-secondary: #f9fafb;
12
+ --bg-tertiary: #f3f4f6;
13
+ --bg-hover: #e5e7eb;
14
+ --border-color: #e5e7eb;
15
+ --border-subtle: #f3f4f6;
16
+ --text-primary: #111827;
17
+ --text-secondary: #4b5563;
18
+ --text-muted: #9ca3af;
19
+ --accent-indigo: #6366f1;
20
+ --accent-indigo-light: #e0e7ff;
21
+ --accent-indigo-hover: #4f46e5;
22
+ --accent-emerald: #10b981;
23
+ --accent-emerald-light: #d1fae5;
24
+ --accent-amber: #f59e0b;
25
+ --accent-amber-light: #fef3c7;
26
+ --accent-rose: #ef4444;
27
+ --accent-rose-light: #fee2e2;
28
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
29
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
30
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
31
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
32
+ --radius-sm: 6px;
33
+ --radius-md: 8px;
34
+ --radius-lg: 12px;
35
+ }
36
+
37
+ * {
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ body {
42
+ margin: 0;
43
+ background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 25%, #fdf4ff 50%, #ecfdf5 75%, #f0fdf4 100%);
44
+ color: var(--text-primary);
45
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
46
+ display: flex;
47
+ flex-direction: column;
48
+ height: 100vh;
49
+ overflow: hidden;
50
+ position: relative;
51
+ }
52
+
53
+ /* Multiple ambient gradient orbs */
54
+ body::before {
55
+ content: '';
56
+ position: fixed;
57
+ top: -15%;
58
+ right: -10%;
59
+ width: 60vw;
60
+ height: 60vw;
61
+ background: radial-gradient(circle, rgba(99, 102, 241, 0.25) 0%, rgba(139, 92, 246, 0.15) 30%, transparent 70%);
62
+ filter: blur(100px);
63
+ pointer-events: none;
64
+ z-index: 0;
65
+ animation: float 20s ease-in-out infinite;
66
+ }
67
+
68
+ body::after {
69
+ content: '';
70
+ position: fixed;
71
+ bottom: -15%;
72
+ left: -10%;
73
+ width: 50vw;
74
+ height: 50vw;
75
+ background: radial-gradient(circle, rgba(16, 185, 129, 0.2) 0%, rgba(34, 197, 94, 0.12) 30%, transparent 70%);
76
+ filter: blur(100px);
77
+ pointer-events: none;
78
+ z-index: 0;
79
+ animation: float 25s ease-in-out infinite reverse;
80
+ }
81
+
82
+ @keyframes float {
83
+ 0%, 100% { transform: translate(0, 0) scale(1); }
84
+ 33% { transform: translate(30px, -30px) scale(1.05); }
85
+ 66% { transform: translate(-20px, 20px) scale(0.95); }
86
+ }
87
+
88
+ /* Page load animation */
89
+ @keyframes fadeSlideIn {
90
+ from {
91
+ opacity: 0;
92
+ transform: translateY(8px);
93
+ }
94
+ to {
95
+ opacity: 1;
96
+ transform: translateY(0);
97
+ }
98
+ }
99
+
100
+ body > * {
101
+ animation: fadeSlideIn 0.3s ease-out backwards;
102
+ }
103
+
104
+ header {
105
+ padding: 1rem 1.5rem;
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 1rem;
109
+ border-bottom: 1px solid rgba(99, 102, 241, 0.1);
110
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.85) 0%, rgba(238, 242, 255, 0.75) 50%, rgba(250, 245, 255, 0.85) 100%);
111
+ backdrop-filter: blur(20px);
112
+ flex-shrink: 0;
113
+ position: relative;
114
+ z-index: 1;
115
+ box-shadow: 0 4px 30px rgba(99, 102, 241, 0.1);
116
+ }
117
+
118
+ header h1 {
119
+ font-size: 1.1rem;
120
+ margin: 0;
121
+ font-weight: 600;
122
+ color: var(--text-primary);
123
+ letter-spacing: -0.01em;
124
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #ec4899 100%);
125
+ -webkit-background-clip: text;
126
+ -webkit-text-fill-color: transparent;
127
+ background-clip: text;
128
+ }
129
+
130
+ main {
131
+ flex: 1;
132
+ display: grid;
133
+ grid-template-columns: var(--sidebar-width, 380px) 4px 1fr;
134
+ background: transparent;
135
+ min-height: 0;
136
+ overflow: hidden;
137
+ position: relative;
138
+ z-index: 1;
139
+ }
140
+
141
+ aside {
142
+ background: linear-gradient(180deg, rgba(238, 242, 255, 0.5) 0%, rgba(250, 245, 255, 0.4) 50%, rgba(236, 253, 245, 0.5) 100%);
143
+ backdrop-filter: blur(15px);
144
+ overflow-y: auto;
145
+ overflow-x: hidden;
146
+ min-width: 200px;
147
+ max-width: 800px;
148
+ height: 100%;
149
+ border-right: 1px solid rgba(99, 102, 241, 0.15);
150
+ }
151
+
152
+ aside::-webkit-scrollbar {
153
+ width: 6px;
154
+ }
155
+
156
+ aside::-webkit-scrollbar-track {
157
+ background: transparent;
158
+ }
159
+
160
+ aside::-webkit-scrollbar-thumb {
161
+ background: var(--border-color);
162
+ border-radius: 3px;
163
+ }
164
+
165
+ aside::-webkit-scrollbar-thumb:hover {
166
+ background: var(--text-muted);
167
+ }
168
+
169
+ .resizer {
170
+ background: var(--border-color);
171
+ cursor: col-resize;
172
+ position: relative;
173
+ user-select: none;
174
+ transition: all 0.2s ease;
175
+ }
176
+
177
+ .resizer:hover,
178
+ .resizer.active {
179
+ background: var(--accent-indigo);
180
+ }
181
+
182
+ .resizer::after {
183
+ content: '';
184
+ position: absolute;
185
+ left: 50%;
186
+ top: 50%;
187
+ transform: translate(-50%, -50%);
188
+ width: 3px;
189
+ height: 32px;
190
+ background: var(--text-muted);
191
+ border-radius: 2px;
192
+ opacity: 0;
193
+ transition: opacity 0.2s ease;
194
+ }
195
+
196
+ .resizer:hover::after,
197
+ .resizer.active::after {
198
+ opacity: 1;
199
+ background: white;
200
+ }
201
+
202
+ .global-controls {
203
+ padding: 0.75rem 1rem;
204
+ border-bottom: 1px solid var(--border-color);
205
+ display: flex;
206
+ gap: 0.5rem;
207
+ background: var(--bg-secondary);
208
+ position: sticky;
209
+ top: 0;
210
+ z-index: 10;
211
+ }
212
+
213
+ .global-controls .secondary {
214
+ flex: 1;
215
+ padding: 0.45rem 0.65rem;
216
+ font-size: 0.7rem;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ gap: 0.3rem;
221
+ font-weight: 500;
222
+ }
223
+
224
+ .global-controls .secondary:hover {
225
+ background: var(--accent-indigo-light);
226
+ border-color: var(--accent-indigo);
227
+ color: var(--accent-indigo);
228
+ }
229
+
230
+ /* Tree view styles */
231
+ .tree-node {
232
+ user-select: none;
233
+ }
234
+
235
+ .tree-node-content {
236
+ display: flex;
237
+ align-items: center;
238
+ padding: 0.4rem 0.6rem;
239
+ cursor: pointer;
240
+ transition: all 0.2s ease;
241
+ border-bottom: 1px solid var(--border-subtle);
242
+ position: relative;
243
+ }
244
+
245
+ .tree-node-content::before {
246
+ content: '';
247
+ position: absolute;
248
+ inset: 0;
249
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(236, 72, 153, 0.1) 100%);
250
+ opacity: 0;
251
+ transition: opacity 0.2s ease;
252
+ border-radius: var(--radius-sm);
253
+ }
254
+
255
+ .tree-node-content:hover::before {
256
+ opacity: 1;
257
+ }
258
+
259
+ .tree-node-content > * {
260
+ position: relative;
261
+ z-index: 1;
262
+ }
263
+
264
+ .tree-node-content.selected {
265
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(236, 72, 153, 0.15) 100%);
266
+ box-shadow: 0 4px 15px rgba(139, 92, 246, 0.25);
267
+ }
268
+
269
+ .tree-expand-icon {
270
+ width: 18px;
271
+ height: 18px;
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: center;
275
+ margin-right: 0.25rem;
276
+ transition: transform 0.2s ease;
277
+ cursor: pointer;
278
+ color: var(--text-muted);
279
+ font-size: 0.65rem;
280
+ }
281
+
282
+ .tree-expand-icon.expanded {
283
+ transform: rotate(90deg);
284
+ }
285
+
286
+ .tree-expand-icon.hidden {
287
+ visibility: hidden;
288
+ }
289
+
290
+ .tree-node-checkbox {
291
+ appearance: none;
292
+ -webkit-appearance: none;
293
+ width: 16px;
294
+ height: 16px;
295
+ cursor: pointer;
296
+ margin-right: 0.5rem;
297
+ border: 2px solid var(--border-color);
298
+ border-radius: 4px;
299
+ background: var(--bg-primary);
300
+ position: relative;
301
+ transition: all 0.15s ease;
302
+ flex-shrink: 0;
303
+ }
304
+
305
+ .tree-node-checkbox:hover {
306
+ border-color: var(--accent-emerald);
307
+ }
308
+
309
+ .tree-node-checkbox:checked {
310
+ background: var(--bg-primary);
311
+ border-color: var(--accent-emerald);
312
+ }
313
+
314
+ .tree-node-checkbox:checked::after {
315
+ content: '';
316
+ position: absolute;
317
+ top: 50%;
318
+ left: 50%;
319
+ width: 3px;
320
+ height: 6px;
321
+ border: solid var(--accent-emerald);
322
+ border-width: 0 2px 2px 0;
323
+ transform: translate(-50%, -60%) rotate(45deg);
324
+ }
325
+
326
+ .tree-node-checkbox:indeterminate {
327
+ background: var(--bg-primary);
328
+ border-color: var(--accent-emerald);
329
+ }
330
+
331
+ .tree-node-checkbox:indeterminate::after {
332
+ content: '';
333
+ position: absolute;
334
+ top: 50%;
335
+ left: 50%;
336
+ width: 8px;
337
+ height: 2px;
338
+ background: var(--accent-emerald);
339
+ transform: translate(-50%, -50%);
340
+ }
341
+
342
+ .tree-node-label {
343
+ flex: 1;
344
+ font-size: 0.82rem;
345
+ white-space: nowrap;
346
+ overflow: hidden;
347
+ text-overflow: ellipsis;
348
+ min-width: 0;
349
+ }
350
+
351
+ .tree-node-label.folder {
352
+ font-weight: 600;
353
+ color: var(--text-primary);
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 0.25rem;
357
+ }
358
+
359
+ .tree-node-label.folder .tree-node-count {
360
+ flex-shrink: 0;
361
+ }
362
+
363
+ .tree-node-label.file {
364
+ color: var(--text-secondary);
365
+ }
366
+
367
+ .tree-node-method {
368
+ font-size: 0.65rem;
369
+ padding: 0.15rem 0.45rem;
370
+ border-radius: var(--radius-sm);
371
+ margin-right: 0.4rem;
372
+ font-weight: 600;
373
+ text-transform: uppercase;
374
+ letter-spacing: 0.03em;
375
+ }
376
+
377
+ .tree-node-method.get {
378
+ background: linear-gradient(135deg, var(--accent-emerald-light) 0%, rgba(16, 185, 129, 0.15) 100%);
379
+ color: #047857;
380
+ box-shadow: 0 2px 6px rgba(16, 185, 129, 0.15);
381
+ }
382
+
383
+ .tree-node-method.post {
384
+ background: linear-gradient(135deg, var(--accent-amber-light) 0%, rgba(245, 158, 11, 0.15) 100%);
385
+ color: #b45309;
386
+ box-shadow: 0 2px 6px rgba(245, 158, 11, 0.15);
387
+ }
388
+
389
+ .tree-node-method.put {
390
+ background: linear-gradient(135deg, var(--accent-indigo-light) 0%, rgba(99, 102, 241, 0.15) 100%);
391
+ color: #4338ca;
392
+ box-shadow: 0 2px 6px rgba(99, 102, 241, 0.15);
393
+ }
394
+
395
+ .tree-node-method.delete {
396
+ background: linear-gradient(135deg, var(--accent-rose-light) 0%, rgba(239, 68, 68, 0.15) 100%);
397
+ color: #b91c1c;
398
+ box-shadow: 0 2px 6px rgba(239, 68, 68, 0.15);
399
+ }
400
+
401
+ .tree-node-method.patch {
402
+ background: linear-gradient(135deg, #ede9fe 0%, rgba(139, 92, 246, 0.15) 100%);
403
+ color: #7c3aed;
404
+ box-shadow: 0 2px 6px rgba(139, 92, 246, 0.15);
405
+ }
406
+
407
+ .tree-children {
408
+ padding-left: 1rem;
409
+ display: none;
410
+ }
411
+
412
+ .tree-children.expanded {
413
+ display: block;
414
+ }
415
+
416
+ .tree-node-count {
417
+ font-size: 0.7rem;
418
+ color: var(--text-muted);
419
+ margin-left: 0.4rem;
420
+ }
421
+
422
+ .tree-node-delete {
423
+ display: none;
424
+ align-items: center;
425
+ justify-content: center;
426
+ width: 18px;
427
+ height: 18px;
428
+ margin-left: auto;
429
+ border-radius: var(--radius-sm);
430
+ cursor: pointer;
431
+ color: var(--accent-rose);
432
+ font-size: 0.85rem;
433
+ transition: all 0.15s ease;
434
+ user-select: none;
435
+ }
436
+
437
+ .tree-node-delete:hover {
438
+ background: var(--accent-rose-light);
439
+ }
440
+
441
+ .tree-node-content:hover .tree-node-delete {
442
+ display: flex;
443
+ }
444
+
445
+ #mock-details {
446
+ height: calc(100% - 60px);
447
+ }
448
+
449
+ section {
450
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.6) 0%, rgba(238, 242, 255, 0.5) 50%, rgba(250, 245, 255, 0.6) 100%);
451
+ backdrop-filter: blur(15px);
452
+ padding: 1.5rem;
453
+ overflow-y: auto;
454
+ overflow-x: hidden;
455
+ display: flex;
456
+ flex-direction: column;
457
+ gap: 1rem;
458
+ height: 100%;
459
+ }
460
+
461
+ section::-webkit-scrollbar {
462
+ width: 6px;
463
+ }
464
+
465
+ section::-webkit-scrollbar-track {
466
+ background: transparent;
467
+ }
468
+
469
+ section::-webkit-scrollbar-thumb {
470
+ background: var(--border-color);
471
+ border-radius: 3px;
472
+ }
473
+
474
+ section::-webkit-scrollbar-thumb:hover {
475
+ background: var(--text-muted);
476
+ }
477
+
478
+ section > h3 {
479
+ margin: 0;
480
+ flex-shrink: 0;
481
+ color: var(--text-primary);
482
+ font-size: 0.85rem;
483
+ font-weight: 600;
484
+ text-transform: uppercase;
485
+ letter-spacing: 0.05em;
486
+ }
487
+
488
+ section .data-container {
489
+ flex: 1 1 auto;
490
+ min-height: 0;
491
+ display: flex;
492
+ flex-direction: column;
493
+ overflow: hidden;
494
+ height: 100%;
495
+ }
496
+
497
+ .controls {
498
+ display: flex;
499
+ flex-wrap: wrap;
500
+ gap: 1rem;
501
+ align-items: flex-start;
502
+ flex-shrink: 0;
503
+ }
504
+
505
+ .controls h2 {
506
+ width: 100%;
507
+ margin: 0 0 0.75rem 0;
508
+ font-size: 1rem;
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 0.75rem;
512
+ }
513
+
514
+ .controls label input[type="text"] {
515
+ padding: 0.4rem 0.6rem;
516
+ border-radius: var(--radius-sm);
517
+ border: 1px solid var(--border-color);
518
+ background: var(--bg-primary);
519
+ color: var(--text-primary);
520
+ font-family: inherit;
521
+ font-size: 0.8rem;
522
+ outline: none;
523
+ transition: all 0.15s ease;
524
+ }
525
+
526
+ .controls label input[type="text"]:focus {
527
+ border-color: var(--accent-indigo);
528
+ box-shadow: 0 0 0 3px var(--accent-indigo-light);
529
+ }
530
+
531
+ .badge {
532
+ display: inline-flex;
533
+ align-items: center;
534
+ gap: 0.25rem;
535
+ border-radius: var(--radius-sm);
536
+ padding: 0.25rem 0.65rem;
537
+ font-size: 0.65rem;
538
+ font-weight: 600;
539
+ background: linear-gradient(135deg, #818cf8 0%, #a78bfa 50%, #f472b6 100%);
540
+ color: white;
541
+ text-transform: uppercase;
542
+ letter-spacing: 0.05em;
543
+ box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
544
+ }
545
+
546
+ textarea {
547
+ width: 100%;
548
+ flex: 1;
549
+ min-height: 300px;
550
+ font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
551
+ font-size: 0.82rem;
552
+ padding: 1rem;
553
+ border: 1px solid var(--border-color);
554
+ border-radius: var(--radius-md);
555
+ resize: none;
556
+ background: var(--bg-secondary);
557
+ color: var(--text-primary);
558
+ overflow-y: auto;
559
+ outline: none;
560
+ transition: all 0.15s ease;
561
+ line-height: 1.6;
562
+ }
563
+
564
+ textarea:focus {
565
+ border-color: var(--accent-indigo);
566
+ box-shadow: 0 0 0 3px var(--accent-indigo-light);
567
+ }
568
+
569
+ label {
570
+ font-size: 0.75rem;
571
+ color: var(--text-secondary);
572
+ display: flex;
573
+ gap: 0.5rem;
574
+ align-items: center;
575
+ }
576
+
577
+ input[type="number"] {
578
+ width: 90px;
579
+ padding: 0.35rem 0.5rem;
580
+ border-radius: var(--radius-sm);
581
+ border: 1px solid var(--border-color);
582
+ background: var(--bg-primary);
583
+ color: var(--text-primary);
584
+ font-family: inherit;
585
+ font-size: 0.8rem;
586
+ outline: none;
587
+ transition: all 0.15s ease;
588
+ }
589
+
590
+ input[type="number"]:focus {
591
+ border-color: var(--accent-indigo);
592
+ box-shadow: 0 0 0 3px var(--accent-indigo-light);
593
+ }
594
+
595
+ .actions {
596
+ display: flex;
597
+ gap: 0.75rem;
598
+ flex-shrink: 0;
599
+ margin-top: 0.5rem;
600
+ }
601
+
602
+ button.primary {
603
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%);
604
+ color: white;
605
+ padding: 0.5rem 1rem;
606
+ border-radius: var(--radius-md);
607
+ border: none;
608
+ cursor: pointer;
609
+ font-weight: 500;
610
+ font-family: inherit;
611
+ font-size: 0.8rem;
612
+ transition: all 0.2s ease;
613
+ box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4);
614
+ display: inline-flex;
615
+ align-items: center;
616
+ gap: 0.4rem;
617
+ position: relative;
618
+ overflow: hidden;
619
+ }
620
+
621
+ button.primary::before {
622
+ content: '';
623
+ position: absolute;
624
+ inset: 0;
625
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%);
626
+ opacity: 0;
627
+ transition: opacity 0.3s ease;
628
+ }
629
+
630
+ button.primary:hover::before {
631
+ opacity: 1;
632
+ }
633
+
634
+ button.primary .btn-icon {
635
+ color: white;
636
+ }
637
+
638
+ button.primary:hover {
639
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #9333ea 100%);
640
+ box-shadow: 0 6px 25px rgba(139, 92, 246, 0.5);
641
+ transform: translateY(-2px);
642
+ }
643
+
644
+ button.secondary {
645
+ background: var(--bg-primary);
646
+ color: var(--text-secondary);
647
+ padding: 0.5rem 1rem;
648
+ border-radius: var(--radius-md);
649
+ border: 1px solid var(--border-color);
650
+ cursor: pointer;
651
+ font-family: inherit;
652
+ font-size: 0.8rem;
653
+ transition: all 0.15s ease;
654
+ }
655
+
656
+ button.secondary:hover {
657
+ background: var(--bg-hover);
658
+ color: var(--text-primary);
659
+ }
660
+
661
+ /* Button icons */
662
+ .btn-icon {
663
+ display: inline-flex;
664
+ align-items: center;
665
+ justify-content: center;
666
+ font-size: 1rem;
667
+ font-weight: 300;
668
+ line-height: 1;
669
+ }
670
+
671
+ .btn-icon-check {
672
+ display: inline-flex;
673
+ align-items: center;
674
+ justify-content: center;
675
+ font-size: 0.85rem;
676
+ font-weight: 600;
677
+ line-height: 1;
678
+ color: var(--accent-emerald);
679
+ }
680
+
681
+ .btn-icon-cross {
682
+ display: inline-flex;
683
+ align-items: center;
684
+ justify-content: center;
685
+ font-size: 0.85rem;
686
+ font-weight: 600;
687
+ line-height: 1;
688
+ color: var(--accent-rose);
689
+ }
690
+
691
+ /* Detail panel checkbox */
692
+ #toggle-enable {
693
+ appearance: none;
694
+ -webkit-appearance: none;
695
+ width: 16px;
696
+ height: 16px;
697
+ cursor: pointer;
698
+ border: 2px solid var(--border-color);
699
+ border-radius: 4px;
700
+ background: var(--bg-primary);
701
+ position: relative;
702
+ transition: all 0.15s ease;
703
+ flex-shrink: 0;
704
+ }
705
+
706
+ #toggle-enable:hover {
707
+ border-color: var(--accent-emerald);
708
+ }
709
+
710
+ #toggle-enable:checked {
711
+ background: var(--bg-primary);
712
+ border-color: var(--accent-emerald);
713
+ }
714
+
715
+ #toggle-enable:checked::after {
716
+ content: '';
717
+ position: absolute;
718
+ top: 50%;
719
+ left: 50%;
720
+ width: 3px;
721
+ height: 6px;
722
+ border: solid var(--accent-emerald);
723
+ border-width: 0 2px 2px 0;
724
+ transform: translate(-50%, -60%) rotate(45deg);
725
+ }
726
+
727
+ .empty {
728
+ display: flex;
729
+ flex-direction: column;
730
+ align-items: center;
731
+ justify-content: center;
732
+ color: var(--text-muted);
733
+ gap: 0.5rem;
734
+ height: 100%;
735
+ font-size: 0.85rem;
736
+ }
737
+
738
+ pre {
739
+ background: var(--bg-secondary);
740
+ padding: 1rem;
741
+ border-radius: var(--radius-md);
742
+ font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
743
+ overflow: auto;
744
+ flex: 1;
745
+ margin: 0;
746
+ min-height: 300px;
747
+ color: var(--text-primary);
748
+ font-size: 0.82rem;
749
+ line-height: 1.6;
750
+ border: 1px solid var(--border-color);
751
+ }
752
+
753
+ textarea.error {
754
+ border-color: var(--accent-rose);
755
+ box-shadow: 0 0 0 3px var(--accent-rose-light);
756
+ }
757
+
758
+ /* Modal styles */
759
+ .modal-overlay {
760
+ position: fixed;
761
+ top: 0;
762
+ left: 0;
763
+ right: 0;
764
+ bottom: 0;
765
+ background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.4) 100%);
766
+ backdrop-filter: blur(8px);
767
+ display: none;
768
+ align-items: center;
769
+ justify-content: center;
770
+ z-index: 1000;
771
+ }
772
+
773
+ .modal-overlay.show {
774
+ display: flex;
775
+ animation: modalFadeIn 0.2s ease-out;
776
+ }
777
+
778
+ @keyframes modalFadeIn {
779
+ from {
780
+ opacity: 0;
781
+ }
782
+ to {
783
+ opacity: 1;
784
+ }
785
+ }
786
+
787
+ .modal {
788
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(238, 242, 255, 0.9) 50%, rgba(250, 245, 255, 0.95) 100%);
789
+ backdrop-filter: blur(25px);
790
+ border: 1px solid rgba(255, 255, 255, 0.6);
791
+ border-radius: var(--radius-lg);
792
+ padding: 2rem;
793
+ max-width: 500px;
794
+ width: 90%;
795
+ box-shadow: 0 25px 50px rgba(139, 92, 246, 0.25), 0 0 100px rgba(236, 72, 153, 0.15);
796
+ animation: modalSlideUp 0.3s ease-out;
797
+ position: relative;
798
+ }
799
+
800
+ .modal::before {
801
+ content: '';
802
+ position: absolute;
803
+ inset: -2px;
804
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(236, 72, 153, 0.3), rgba(59, 130, 246, 0.3));
805
+ border-radius: calc(var(--radius-lg) + 2px);
806
+ z-index: -1;
807
+ opacity: 0.5;
808
+ }
809
+
810
+ @keyframes modalSlideUp {
811
+ from {
812
+ opacity: 0;
813
+ transform: translateY(16px) scale(0.98);
814
+ }
815
+ to {
816
+ opacity: 1;
817
+ transform: translateY(0) scale(1);
818
+ }
819
+ }
820
+
821
+ .modal h2 {
822
+ margin: 0 0 1.5rem 0;
823
+ color: var(--text-primary);
824
+ font-size: 1.1rem;
825
+ font-weight: 600;
826
+ }
827
+
828
+ .modal .form-group {
829
+ margin-bottom: 1.25rem;
830
+ }
831
+
832
+ .modal .form-group label {
833
+ display: block;
834
+ margin-bottom: 0.5rem;
835
+ font-weight: 500;
836
+ color: var(--text-primary);
837
+ font-size: 0.8rem;
838
+ }
839
+
840
+ .modal .form-group input,
841
+ .modal .form-group select,
842
+ .modal .form-group textarea {
843
+ width: 100%;
844
+ padding: 0.6rem;
845
+ border: 1px solid var(--border-color);
846
+ border-radius: var(--radius-sm);
847
+ font-size: 0.85rem;
848
+ font-family: inherit;
849
+ background: var(--bg-primary);
850
+ color: var(--text-primary);
851
+ outline: none;
852
+ transition: all 0.15s ease;
853
+ }
854
+
855
+ .modal .form-group input:focus,
856
+ .modal .form-group select:focus,
857
+ .modal .form-group textarea:focus {
858
+ border-color: var(--accent-indigo);
859
+ box-shadow: 0 0 0 3px var(--accent-indigo-light);
860
+ }
861
+
862
+ .modal .form-group textarea {
863
+ min-height: 100px;
864
+ font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
865
+ font-size: 0.8rem;
866
+ }
867
+
868
+ .modal .form-group small {
869
+ display: block;
870
+ margin-top: 0.35rem;
871
+ color: var(--text-muted);
872
+ font-size: 0.7rem;
873
+ }
874
+
875
+ .modal .form-actions {
876
+ display: flex;
877
+ gap: 1rem;
878
+ justify-content: flex-end;
879
+ margin-top: 1.5rem;
880
+ }
881
+
882
+ /* Animation delays for stagger effect */
883
+ header { animation-delay: 0.05s; }
884
+ main { animation-delay: 0.1s; }
885
+ </style>
886
+ </head>
887
+ <body>
888
+ <header>
889
+ <h1>Mock Inspector</h1>
890
+ <span class="badge">__API_PREFIX__</span>
891
+ <button id="new-api-btn" class="primary" style="margin-left: auto;"><span class="btn-icon">+</span> New API</button>
892
+ </header>
893
+ <main>
894
+ <aside id="sidebar">
895
+ <div class="global-controls">
896
+ <button id="enable-all" class="secondary"><span class="btn-icon-check">✓</span> 开启所有</button>
897
+ <button id="disable-all" class="secondary"><span class="btn-icon-cross">✗</span> 关闭所有</button>
898
+ </div>
899
+ <ul id="mock-list"></ul>
900
+ </aside>
901
+ <div class="resizer" id="resizer"></div>
902
+ <section>
903
+ <div id="mock-details" class="empty">
904
+ <p>Select a mock entry to inspect</p>
905
+ </div>
906
+ </section>
907
+ </main>
908
+
909
+ <div id="new-api-modal" class="modal-overlay">
910
+ <div class="modal">
911
+ <h2><span class="btn-icon">+</span> New API Mock</h2>
912
+ <form id="new-api-form">
913
+ <div class="form-group">
914
+ <label for="new-api-method">HTTP Method</label>
915
+ <select id="new-api-method" required>
916
+ <option value="get">GET</option>
917
+ <option value="post">POST</option>
918
+ <option value="put">PUT</option>
919
+ <option value="delete">DELETE</option>
920
+ <option value="patch">PATCH</option>
921
+ </select>
922
+ </div>
923
+ <div class="form-group">
924
+ <label for="new-api-path">API Path (without prefix)</label>
925
+ <input type="text" id="new-api-path" placeholder="/users/list" required />
926
+ <small style="color: rgba(15, 23, 42, 0.6); font-size: 0.85rem;">例如:/users/list 或 /api/items</small>
927
+ </div>
928
+ <div class="form-group">
929
+ <label for="new-api-description">Description (Optional)</label>
930
+ <input type="text" id="new-api-description" placeholder="例如:用户列表接口" />
931
+ </div>
932
+ <div class="form-group">
933
+ <label for="new-api-data">Response Data (JSON)</label>
934
+ <textarea id="new-api-data" placeholder='{ "code": 200, "data": [] }'>{ "code": 200, "data": [] }</textarea>
935
+ </div>
936
+ <div class="form-actions">
937
+ <button type="button" id="cancel-new-api">Cancel</button>
938
+ <button type="submit" class="primary">Create</button>
939
+ </div>
940
+ </form>
941
+ </div>
942
+ </div>
943
+
944
+ <script>
945
+ const inspectorRoute = __ROUTE_JSON__;
946
+ const apiBase = (inspectorRoute.endsWith('/') ? inspectorRoute.slice(0, -1) : inspectorRoute) + '/api';
947
+ const allowToggle = __ALLOW_TOGGLE_JSON__;
948
+
949
+ function escapeHtml(value) {
950
+ if (value == null) return '';
951
+ return String(value)
952
+ .replace(/&/g, '&amp;')
953
+ .replace(/</g, '&lt;')
954
+ .replace(/>/g, '&gt;')
955
+ .replace(/"/g, '&quot;')
956
+ .replace(/'/g, '&#39;');
957
+ }
958
+
959
+ // Tree data structure conversion
960
+ function buildMockTree(mocks) {
961
+ const root = { id: 'root', name: 'root', type: 'folder', children: [], checked: false, indeterminate: false };
962
+
963
+ mocks.forEach(mock => {
964
+ // Parse the file path to build tree structure
965
+ // Example: "automock/api/v1/asset-groups/prod-db-redis/put.js"
966
+ const parts = mock.file.split('/').filter(p => p);
967
+ let currentNode = root;
968
+
969
+ parts.forEach((part, index) => {
970
+ const isFile = part.endsWith('.js') || part.endsWith('.ts');
971
+ const nodeName = isFile ? part.replace(/\.(js|ts)$/, '') : part;
972
+ const nodeId = parts.slice(0, index + 1).join('/');
973
+
974
+ let childNode = currentNode.children.find(child => child.name === nodeName);
975
+
976
+ if (!childNode) {
977
+ childNode = {
978
+ id: nodeId,
979
+ name: nodeName,
980
+ type: isFile ? 'file' : 'folder',
981
+ level: index,
982
+ children: [],
983
+ checked: false,
984
+ indeterminate: false
985
+ };
986
+
987
+ if (isFile) {
988
+ childNode.mockInfo = mock;
989
+ }
990
+
991
+ currentNode.children.push(childNode);
992
+ }
993
+
994
+ currentNode = childNode;
995
+ });
996
+ });
997
+
998
+ // Initialize checked state based on mock.config.enable
999
+ function initCheckedState(node) {
1000
+ if (node.type === 'file' && node.mockInfo) {
1001
+ node.checked = node.mockInfo.config.enable || false;
1002
+ node.indeterminate = false;
1003
+ }
1004
+ if (node.children && node.children.length > 0) {
1005
+ node.children.forEach(initCheckedState);
1006
+ }
1007
+ }
1008
+
1009
+ initCheckedState(root);
1010
+
1011
+ // Calculate parent states
1012
+ function updateParentStates(node) {
1013
+ if (node.children && node.children.length > 0) {
1014
+ node.children.forEach(updateParentStates);
1015
+
1016
+ const allChecked = node.children.every(child => child.checked && !child.indeterminate);
1017
+ const someChecked = node.children.some(child => child.checked || child.indeterminate);
1018
+
1019
+ node.checked = allChecked;
1020
+ node.indeterminate = !allChecked && someChecked;
1021
+ }
1022
+ }
1023
+
1024
+ updateParentStates(root);
1025
+
1026
+ return root.children;
1027
+ }
1028
+
1029
+ // Get all leaf node (file) keys under a node
1030
+ function getAllFileKeys(node) {
1031
+ if (node.type === 'file') {
1032
+ return [node.mockInfo?.key].filter(Boolean);
1033
+ }
1034
+
1035
+ if (node.children && node.children.length > 0) {
1036
+ const keys = [];
1037
+ node.children.forEach(child => {
1038
+ keys.push(...getAllFileKeys(child));
1039
+ });
1040
+ return keys;
1041
+ }
1042
+
1043
+ return [];
1044
+ }
1045
+
1046
+ // Update tree node state recursively
1047
+ function updateTreeNodeState(node, checked, updateChildren = true) {
1048
+ if (updateChildren && node.children && node.children.length > 0) {
1049
+ node.children.forEach(child => {
1050
+ updateTreeNodeState(child, checked, true);
1051
+ });
1052
+ }
1053
+
1054
+ node.checked = checked;
1055
+ node.indeterminate = false;
1056
+ }
1057
+
1058
+ // Update parent states bottom-up
1059
+ function updateParentNodeState(node, tree) {
1060
+ // Find parent node
1061
+ function findParent(n, targetId, parent = null) {
1062
+ if (n.id === targetId) return parent;
1063
+ if (n.children) {
1064
+ for (const child of n.children) {
1065
+ const result = findParent(child, targetId, n);
1066
+ if (result) return result;
1067
+ }
1068
+ }
1069
+ return null;
1070
+ }
1071
+
1072
+ const parent = findParent({ children: tree }, node.id);
1073
+
1074
+ if (parent) {
1075
+ const allChecked = parent.children.every(child => child.checked && !child.indeterminate);
1076
+ const someChecked = parent.children.some(child => child.checked || child.indeterminate);
1077
+
1078
+ parent.checked = allChecked;
1079
+ parent.indeterminate = !allChecked && someChecked;
1080
+
1081
+ updateParentNodeState(parent, tree);
1082
+ }
1083
+ }
1084
+
1085
+ // Render tree node recursively
1086
+ function renderTreeNode(node, level = 0, expandedNodes = new Set()) {
1087
+ const hasChildren = node.children && node.children.length > 0;
1088
+ const isExpanded = expandedNodes.has(node.id);
1089
+ const paddingLeft = level * 1.2 + 0.5;
1090
+
1091
+ let html = '<div class="tree-node" data-node-id="' + escapeHtml(node.id) + '" data-node-type="' + node.type + '">';
1092
+
1093
+ // Node content
1094
+ html += '<div class="tree-node-content" style="padding-left: ' + paddingLeft + 'rem">';
1095
+
1096
+ // Expand/collapse icon
1097
+ if (hasChildren) {
1098
+ html += '<span class="tree-expand-icon' + (isExpanded ? ' expanded' : '') + '">▶</span>';
1099
+ } else {
1100
+ html += '<span class="tree-expand-icon hidden"></span>';
1101
+ }
1102
+
1103
+ // Checkbox
1104
+ const checkedAttr = node.checked ? 'checked' : '';
1105
+ const indeterminateAttr = node.indeterminate ? 'data-indeterminate="true"' : '';
1106
+ html += '<input type="checkbox" class="tree-node-checkbox" ' + checkedAttr + ' ' + indeterminateAttr + ' />';
1107
+
1108
+ // Node label
1109
+ if (node.type === 'file' && node.mockInfo) {
1110
+ const mock = node.mockInfo;
1111
+ const methodClass = mock.method?.toLowerCase() || 'get';
1112
+ html += '<span class="tree-node-method ' + methodClass + '">' + escapeHtml(mock.method?.toUpperCase() || 'GET') + '</span>';
1113
+ html += '<span class="tree-node-label file" title="' + escapeHtml(mock.path) + '">' + escapeHtml(mock.path) + '</span>';
1114
+ if (mock.description) {
1115
+ html += '<span class="tree-node-count" title="' + escapeHtml(mock.description) + '">' + escapeHtml(mock.description) + '</span>';
1116
+ }
1117
+ // Delete button (only for files)
1118
+ html += '<span class="tree-node-delete" data-mock-key="' + escapeHtml(mock.key) + '" data-is-folder="false" title="删除此 Mock">✕</span>';
1119
+ } else {
1120
+ const fileCount = getAllFileKeys(node);
1121
+ const fileKeysJson = JSON.stringify(fileCount);
1122
+ // Put count inside the label to avoid layout shift when delete button appears
1123
+ html += '<span class="tree-node-label folder">' + escapeHtml(node.name) + ' <span class="tree-node-count">(' + fileCount.length + ')</span></span>';
1124
+ // Delete button for folders
1125
+ html += '<span class="tree-node-delete" data-mock-keys="' + escapeHtml(fileKeysJson) + '" data-is-folder="true" data-folder-name="' + escapeHtml(node.name) + '" title="删除此文件夹及所有 Mock">✕</span>';
1126
+ }
1127
+
1128
+ html += '</div>';
1129
+
1130
+ // Children container
1131
+ if (hasChildren) {
1132
+ html += '<div class="tree-children' + (isExpanded ? ' expanded' : '') + '">';
1133
+ node.children.forEach(child => {
1134
+ html += renderTreeNode(child, level + 1, expandedNodes);
1135
+ });
1136
+ html += '</div>';
1137
+ }
1138
+
1139
+ html += '</div>';
1140
+
1141
+ return html;
1142
+ }
1143
+
1144
+ // Find node by id in tree
1145
+ function findNodeById(nodes, id) {
1146
+ for (const node of nodes) {
1147
+ if (node.id === id) return node;
1148
+ if (node.children) {
1149
+ const found = findNodeById(node.children, id);
1150
+ if (found) return found;
1151
+ }
1152
+ }
1153
+ return null;
1154
+ }
1155
+
1156
+ // Toggle tree node expand/collapse
1157
+ function toggleTreeNodeExpand(nodeId) {
1158
+ const nodeEl = document.querySelector('.tree-node[data-node-id="' + nodeId + '"]');
1159
+ if (!nodeEl) return;
1160
+
1161
+ const childrenEl = nodeEl.querySelector('.tree-children');
1162
+ const iconEl = nodeEl.querySelector('.tree-expand-icon');
1163
+
1164
+ if (childrenEl && iconEl && !iconEl.classList.contains('hidden')) {
1165
+ const isExpanded = childrenEl.classList.contains('expanded');
1166
+ if (isExpanded) {
1167
+ childrenEl.classList.remove('expanded');
1168
+ iconEl.classList.remove('expanded');
1169
+ } else {
1170
+ childrenEl.classList.add('expanded');
1171
+ iconEl.classList.add('expanded');
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ function renderToggleSection(mock) {
1177
+ if (!allowToggle) {
1178
+ return '';
1179
+ }
1180
+ const checked = mock.config.enable ? 'checked' : '';
1181
+ return (
1182
+ '<label>' +
1183
+ '<input type="checkbox" id="toggle-enable" ' + checked + ' />' +
1184
+ 'Enable' +
1185
+ '</label>'
1186
+ );
1187
+ }
1188
+
1189
+ function renderDataSection(mock) {
1190
+ if (mock.editable) {
1191
+ return '<div class="data-container"><textarea id="data-editor"></textarea><div class="actions"><button class="primary" id="save-btn">Save</button></div></div>';
1192
+ }
1193
+ return '<div class="data-container"><pre id="data-preview"></pre></div>';
1194
+ }
1195
+
1196
+ async function fetchMocks() {
1197
+ try {
1198
+ const res = await fetch(apiBase + '/list');
1199
+ if (!res.ok) {
1200
+ throw new Error('HTTP ' + res.status + ': ' + res.statusText);
1201
+ }
1202
+ const data = await res.json();
1203
+ return data.mocks || [];
1204
+ } catch (error) {
1205
+ console.error('[Inspector] Failed to fetch mocks:', error);
1206
+ return [];
1207
+ }
1208
+ }
1209
+
1210
+ function renderMockList(mocks, preserveExpandedState = false) {
1211
+ const list = document.getElementById('mock-list');
1212
+ if (!list) {
1213
+ console.error('[Inspector] Element #mock-list not found!');
1214
+ return;
1215
+ }
1216
+
1217
+ // Save current expanded state if requested
1218
+ let savedExpandedNodes = new Set();
1219
+ if (preserveExpandedState) {
1220
+ list.querySelectorAll('.tree-children.expanded').forEach(el => {
1221
+ const parentNode = el.closest('.tree-node');
1222
+ if (parentNode) {
1223
+ savedExpandedNodes.add(parentNode.dataset.nodeId);
1224
+ }
1225
+ });
1226
+ }
1227
+
1228
+ list.innerHTML = '';
1229
+ if (!mocks || mocks.length === 0) {
1230
+ console.warn('[Inspector] No mocks to display');
1231
+ list.innerHTML = '<div style="padding: 1rem; color: #999;">No mock files found</div>';
1232
+ return;
1233
+ }
1234
+
1235
+ // Build tree structure
1236
+ const tree = buildMockTree(mocks);
1237
+
1238
+ // Use saved expanded state or expand first level by default
1239
+ const expandedNodes = savedExpandedNodes.size > 0 ? savedExpandedNodes : new Set();
1240
+ if (expandedNodes.size === 0) {
1241
+ tree.forEach(node => {
1242
+ if (node.type === 'folder') {
1243
+ expandedNodes.add(node.id);
1244
+ }
1245
+ });
1246
+ }
1247
+
1248
+ let treeHtml = '';
1249
+ tree.forEach(node => {
1250
+ const nodeHtml = renderTreeNode(node, 0, expandedNodes);
1251
+ treeHtml += nodeHtml;
1252
+ });
1253
+
1254
+ list.innerHTML = treeHtml;
1255
+
1256
+ // Attach event listeners
1257
+ attachTreeEventListeners();
1258
+ }
1259
+
1260
+ // Attach event listeners for tree interactions
1261
+ function attachTreeEventListeners() {
1262
+ const list = document.getElementById('mock-list');
1263
+ if (!list) return;
1264
+
1265
+ // Handle expand/collapse clicks
1266
+ list.querySelectorAll('.tree-expand-icon').forEach(icon => {
1267
+ icon.addEventListener('click', (e) => {
1268
+ e.stopPropagation();
1269
+ const nodeEl = icon.closest('.tree-node');
1270
+ if (nodeEl) {
1271
+ const nodeId = nodeEl.dataset.nodeId;
1272
+ toggleTreeNodeExpand(nodeId);
1273
+ }
1274
+ });
1275
+ });
1276
+
1277
+ // Handle checkbox clicks
1278
+ list.querySelectorAll('.tree-node-checkbox').forEach(checkbox => {
1279
+ checkbox.addEventListener('click', async (e) => {
1280
+ e.stopPropagation();
1281
+ const nodeEl = checkbox.closest('.tree-node');
1282
+ if (!nodeEl) return;
1283
+
1284
+ const nodeId = nodeEl.dataset.nodeId;
1285
+ const nodeType = nodeEl.dataset.nodeType;
1286
+ const checked = checkbox.checked;
1287
+
1288
+ // Get current tree data
1289
+ const tree = buildMockTree(currentMocks);
1290
+
1291
+ // Find and update node
1292
+ const node = findNodeById(tree, nodeId);
1293
+ if (node) {
1294
+ // Update children
1295
+ updateTreeNodeState(node, checked, true);
1296
+ // Update parents
1297
+ updateParentNodeState(node, tree);
1298
+
1299
+ // Apply changes to all affected file nodes
1300
+ const affectedKeys = getAllFileKeys(node);
1301
+ await batchToggleMockEnable(affectedKeys, checked);
1302
+
1303
+ // Re-render tree with preserved expanded state
1304
+ renderMockList(currentMocks, true);
1305
+ }
1306
+ });
1307
+ });
1308
+
1309
+ // Handle node content clicks (for selection)
1310
+ list.querySelectorAll('.tree-node-content').forEach(content => {
1311
+ content.addEventListener('click', (e) => {
1312
+ // Don't select if clicking on checkbox or expand icon
1313
+ if (e.target.classList.contains('tree-node-checkbox') ||
1314
+ e.target.classList.contains('tree-expand-icon')) {
1315
+ return;
1316
+ }
1317
+
1318
+ const nodeEl = content.closest('.tree-node');
1319
+ if (!nodeEl) return;
1320
+
1321
+ const nodeId = nodeEl.dataset.nodeId;
1322
+ const nodeType = nodeEl.dataset.nodeType;
1323
+
1324
+ // Remove previous selection
1325
+ document.querySelectorAll('.tree-node-content.selected').forEach(el => {
1326
+ el.classList.remove('selected');
1327
+ });
1328
+
1329
+ // Add selection to current node
1330
+ content.classList.add('selected');
1331
+
1332
+ // For file nodes, select the mock
1333
+ if (nodeType === 'file') {
1334
+ const tree = buildMockTree(currentMocks);
1335
+ const node = findNodeById(tree, nodeId);
1336
+ if (node && node.mockInfo) {
1337
+ selectMock(node.mockInfo.key, content);
1338
+ }
1339
+ }
1340
+ });
1341
+ });
1342
+
1343
+ // Set indeterminate state for checkboxes
1344
+ list.querySelectorAll('.tree-node-checkbox[data-indeterminate="true"]').forEach(cb => {
1345
+ cb.indeterminate = true;
1346
+ });
1347
+
1348
+ // Handle delete button clicks
1349
+ list.querySelectorAll('.tree-node-delete').forEach(deleteBtn => {
1350
+ deleteBtn.addEventListener('click', async (e) => {
1351
+ e.stopPropagation();
1352
+
1353
+ // Check if user has chosen "never ask again"
1354
+ const skipConfirm = localStorage.getItem('mockInspector_skipDeleteConfirm') === 'true';
1355
+
1356
+ const isFolder = deleteBtn.dataset.isFolder === 'true';
1357
+ let confirmMessage = '';
1358
+ let keysToDelete = [];
1359
+
1360
+ if (isFolder) {
1361
+ // Folder deletion
1362
+ const folderName = deleteBtn.dataset.folderName || '';
1363
+ const keysJson = deleteBtn.dataset.mockKeys || '[]';
1364
+ keysToDelete = JSON.parse(keysJson);
1365
+ confirmMessage = '确定要删除文件夹 "' + folderName + '" 及其包含的 ' + keysToDelete.length + ' 个 Mock 文件吗?此操作不可撤销。';
1366
+ } else {
1367
+ // Single file deletion
1368
+ const mockKey = deleteBtn.dataset.mockKey;
1369
+ if (!mockKey) return;
1370
+ keysToDelete = [mockKey];
1371
+ confirmMessage = '确定要删除这个 Mock 数据吗?此操作不可撤销。';
1372
+ }
1373
+
1374
+ // If user chose "never ask again", delete directly
1375
+ if (skipConfirm) {
1376
+ await performDelete(keysToDelete);
1377
+ return;
1378
+ }
1379
+
1380
+ // Otherwise, show custom confirmation dialog
1381
+ showDeleteConfirmDialog(confirmMessage, keysToDelete);
1382
+ });
1383
+ });
1384
+
1385
+ // Custom delete confirmation dialog
1386
+ function showDeleteConfirmDialog(message, keysToDelete) {
1387
+ const modal = document.getElementById('delete-confirm-modal');
1388
+ const messageEl = document.getElementById('delete-confirm-message');
1389
+ const neverAskCheckbox = document.getElementById('delete-never-ask');
1390
+ const confirmBtn = document.getElementById('delete-confirm-btn');
1391
+ const cancelBtn = document.getElementById('delete-cancel-btn');
1392
+
1393
+ messageEl.textContent = message;
1394
+ neverAskCheckbox.checked = false;
1395
+ modal.classList.add('show');
1396
+
1397
+ // Remove old event listeners
1398
+ const newConfirmBtn = confirmBtn.cloneNode(true);
1399
+ const newCancelBtn = cancelBtn.cloneNode(true);
1400
+ confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
1401
+ cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn);
1402
+
1403
+ // Confirm button handler
1404
+ newConfirmBtn.addEventListener('click', async () => {
1405
+ // Save preference if "never ask again" is checked
1406
+ if (neverAskCheckbox.checked) {
1407
+ localStorage.setItem('mockInspector_skipDeleteConfirm', 'true');
1408
+ }
1409
+ modal.classList.remove('show');
1410
+ await performDelete(keysToDelete);
1411
+ });
1412
+
1413
+ // Cancel button handler
1414
+ newCancelBtn.addEventListener('click', () => {
1415
+ modal.classList.remove('show');
1416
+ });
1417
+
1418
+ // Close on overlay click
1419
+ modal.addEventListener('click', (e) => {
1420
+ if (e.target === modal) {
1421
+ modal.classList.remove('show');
1422
+ }
1423
+ });
1424
+ }
1425
+
1426
+ // Perform the actual deletion
1427
+ async function performDelete(keysToDelete) {
1428
+ try {
1429
+ // Delete all keys (either single file or entire folder)
1430
+ const deletePromises = keysToDelete.map(key =>
1431
+ fetch(apiBase + '/delete?key=' + encodeURIComponent(key), {
1432
+ method: 'DELETE'
1433
+ })
1434
+ );
1435
+
1436
+ const results = await Promise.all(deletePromises);
1437
+
1438
+ // Check if any deletion failed
1439
+ const failedDeletions = results.filter(res => !res.ok);
1440
+ if (failedDeletions.length > 0) {
1441
+ const errors = await Promise.all(failedDeletions.map(res => res.json()));
1442
+ throw new Error(errors.map(e => e.error).join(', '));
1443
+ }
1444
+
1445
+ // Refresh the list
1446
+ currentMocks = await fetchMocks();
1447
+ renderMockList(currentMocks, true);
1448
+
1449
+ // Clear details panel if any deleted mock was selected
1450
+ if (keysToDelete.includes(window.currentKey)) {
1451
+ window.currentKey = null;
1452
+ document.getElementById('mock-details').innerHTML = '<p>Select a mock entry to inspect</p>';
1453
+ document.getElementById('mock-details').className = 'empty';
1454
+ }
1455
+ } catch (error) {
1456
+ alert('删除失败: ' + error.message);
1457
+ }
1458
+ }
1459
+ }
1460
+
1461
+ function renderDetails(mock) {
1462
+ const container = document.getElementById('mock-details');
1463
+ if (!mock) {
1464
+ container.className = 'empty';
1465
+ container.innerHTML = '<p>Select a mock entry to inspect</p>';
1466
+ return;
1467
+ }
1468
+
1469
+ container.className = '';
1470
+ container.innerHTML = [
1471
+ '<div style="display: flex; flex-direction: column; height: 100%;">',
1472
+ ' <div class="controls">',
1473
+ ' <h2>',
1474
+ ' <span class="badge">' + escapeHtml(mock.method.toUpperCase()) + '</span>',
1475
+ ' ' + escapeHtml(mock.path),
1476
+ ' </h2>',
1477
+ ' <label style="flex: 1;" >',
1478
+ ' Desc: ',
1479
+ ' <input type="text" id="description-input" placeholder="例如:用户列表接口" value="' + escapeHtml(mock.description || '') + '" style="width: 100%; margin-top: 0.25rem;" />',
1480
+ ' </label>',
1481
+ ' <label>Delay <input type="number" id="delay-input" value="' + mock.config.delay + '" min="0" step="50" /> ms</label>',
1482
+ ' <label>Status <input type="number" id="status-input" value="' + mock.config.status + '" min="100" max="599" /></label>',
1483
+ ' ' + renderToggleSection(mock),
1484
+ ' </div>',
1485
+ ' <h3>Response Data</h3>',
1486
+ ' ' + renderDataSection(mock),
1487
+ '</div>'
1488
+ ].join('\\n');
1489
+
1490
+ const descriptionInput = document.getElementById('description-input');
1491
+ if (descriptionInput) {
1492
+ descriptionInput.addEventListener('change', () => updateDescription(descriptionInput.value));
1493
+ }
1494
+
1495
+ if (allowToggle) {
1496
+ const enableToggle = document.getElementById('toggle-enable');
1497
+ if (enableToggle) {
1498
+ enableToggle.addEventListener('change', () => updateConfig({ enable: enableToggle.checked }));
1499
+ }
1500
+ }
1501
+
1502
+ const delayInput = document.getElementById('delay-input');
1503
+ if (delayInput) {
1504
+ delayInput.addEventListener('change', () => updateConfig({ delay: Number(delayInput.value) || 0 }));
1505
+ }
1506
+
1507
+ const statusInput = document.getElementById('status-input');
1508
+ if (statusInput) {
1509
+ statusInput.addEventListener('change', () => updateConfig({ status: Number(statusInput.value) || 200 }));
1510
+ }
1511
+
1512
+ if (mock.editable) {
1513
+ const textarea = document.getElementById('data-editor');
1514
+ if (textarea) {
1515
+ textarea.value = mock.dataText || '';
1516
+ const saveBtn = document.getElementById('save-btn');
1517
+ if (saveBtn) {
1518
+ saveBtn.addEventListener('click', async () => {
1519
+ try {
1520
+ const raw = textarea.value || 'null';
1521
+ const parsed = JSON.parse(raw);
1522
+ await updateConfig({ data: parsed });
1523
+ textarea.classList.remove('error');
1524
+ } catch (err) {
1525
+ textarea.classList.add('error');
1526
+ alert('Invalid JSON: ' + err.message);
1527
+ }
1528
+ });
1529
+ }
1530
+ }
1531
+ } else {
1532
+ const pre = document.getElementById('data-preview');
1533
+ if (pre) {
1534
+ pre.textContent = mock.dataText || '';
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ let currentMocks = [];
1540
+
1541
+ async function updateConfig(partial) {
1542
+ if (!window.currentKey) return;
1543
+ if (!allowToggle && 'enable' in partial) {
1544
+ delete partial.enable;
1545
+ }
1546
+ const res = await fetch(apiBase + '/update', {
1547
+ method: 'POST',
1548
+ headers: { 'Content-Type': 'application/json' },
1549
+ body: JSON.stringify({ key: window.currentKey, config: partial })
1550
+ });
1551
+ const data = await res.json();
1552
+ const updated = data.mock;
1553
+ const index = currentMocks.findIndex((item) => item.key === window.currentKey);
1554
+ if (index >= 0) {
1555
+ currentMocks[index] = updated;
1556
+ renderDetails(updated);
1557
+ }
1558
+ }
1559
+
1560
+ async function updateDescription(description) {
1561
+ if (!window.currentKey) return;
1562
+ try {
1563
+ const res = await fetch(apiBase + '/update', {
1564
+ method: 'POST',
1565
+ headers: { 'Content-Type': 'application/json' },
1566
+ body: JSON.stringify({ key: window.currentKey, description: description })
1567
+ });
1568
+ const data = await res.json();
1569
+ const updated = data.mock;
1570
+ const index = currentMocks.findIndex((item) => item.key === window.currentKey);
1571
+ if (index >= 0) {
1572
+ currentMocks[index] = updated;
1573
+ renderDetails(updated);
1574
+ // 重新渲染列表以更新显示
1575
+ renderMockList(currentMocks, true);
1576
+ }
1577
+ } catch (error) {
1578
+ console.error('[Inspector] Failed to update description:', error);
1579
+ alert('更新描述失败: ' + error.message);
1580
+ }
1581
+ }
1582
+
1583
+ async function selectMock(key, button) {
1584
+ window.currentKey = key;
1585
+ document.querySelectorAll('aside button').forEach((btn) => btn.classList.remove('active'));
1586
+ button.classList.add('active');
1587
+ const res = await fetch(apiBase + '/detail?key=' + encodeURIComponent(key));
1588
+ const data = await res.json();
1589
+ const mock = data.mock;
1590
+ const index = currentMocks.findIndex((item) => item.key === key);
1591
+ if (index >= 0) {
1592
+ currentMocks[index] = mock;
1593
+ }
1594
+ renderDetails(mock);
1595
+ }
1596
+
1597
+ async function toggleMockEnable(key, enable) {
1598
+ try {
1599
+ const res = await fetch(apiBase + '/update', {
1600
+ method: 'POST',
1601
+ headers: { 'Content-Type': 'application/json' },
1602
+ body: JSON.stringify({ key: key, config: { enable: enable } })
1603
+ });
1604
+ const data = await res.json();
1605
+ const updated = data.mock;
1606
+ const index = currentMocks.findIndex((item) => item.key === key);
1607
+ if (index >= 0) {
1608
+ currentMocks[index] = updated;
1609
+ // 如果当前正在查看这个 mock,更新详情
1610
+ if (window.currentKey === key) {
1611
+ renderDetails(updated);
1612
+ }
1613
+ }
1614
+ } catch (error) {
1615
+ console.error('[Inspector] Failed to toggle mock:', error);
1616
+ alert('更新失败: ' + error.message);
1617
+ }
1618
+ }
1619
+
1620
+ async function batchToggleMockEnable(keys, enable) {
1621
+ if (keys.length === 0) return;
1622
+ try {
1623
+ const res = await fetch(apiBase + '/batch-update', {
1624
+ method: 'POST',
1625
+ headers: { 'Content-Type': 'application/json' },
1626
+ body: JSON.stringify({ updates: keys.map(key => ({ key, config: { enable } })) })
1627
+ });
1628
+ const data = await res.json();
1629
+ if (data.errors && data.errors.length > 0) {
1630
+ console.warn('[Inspector] Batch update errors:', data.errors);
1631
+ }
1632
+ // Update local data for affected keys
1633
+ for (const key of keys) {
1634
+ const index = currentMocks.findIndex((item) => item.key === key);
1635
+ if (index >= 0) {
1636
+ currentMocks[index].config.enable = enable;
1637
+ }
1638
+ }
1639
+ } catch (error) {
1640
+ console.error('[Inspector] Failed to batch toggle mocks:', error);
1641
+ alert('批量更新失败: ' + error.message);
1642
+ }
1643
+ }
1644
+
1645
+ function startEditDescription(li, mock) {
1646
+ // 清空 li 内容,创建编辑框
1647
+ li.innerHTML = '';
1648
+
1649
+ const input = document.createElement('input');
1650
+ input.type = 'text';
1651
+ input.className = 'description-edit';
1652
+ input.value = mock.description || '';
1653
+ input.placeholder = '输入业务描述,例如:用户列表接口';
1654
+
1655
+ const saveEdit = async () => {
1656
+ const newDescription = input.value.trim();
1657
+ try {
1658
+ const res = await fetch(apiBase + '/update', {
1659
+ method: 'POST',
1660
+ headers: { 'Content-Type': 'application/json' },
1661
+ body: JSON.stringify({ key: mock.key, description: newDescription })
1662
+ });
1663
+ const data = await res.json();
1664
+ const updated = data.mock;
1665
+ const index = currentMocks.findIndex((item) => item.key === mock.key);
1666
+ if (index >= 0) {
1667
+ currentMocks[index] = updated;
1668
+ }
1669
+ // 重新渲染列表
1670
+ renderMockList(currentMocks, true);
1671
+ // 如果当前正在查看这个 mock,更新详情
1672
+ if (window.currentKey === mock.key) {
1673
+ renderDetails(updated);
1674
+ }
1675
+ } catch (error) {
1676
+ console.error('[Inspector] Failed to update description:', error);
1677
+ alert('更新失败: ' + error.message);
1678
+ renderMockList(currentMocks, true);
1679
+ }
1680
+ };
1681
+
1682
+ input.addEventListener('blur', saveEdit);
1683
+ input.addEventListener('keydown', (e) => {
1684
+ if (e.key === 'Enter') {
1685
+ saveEdit();
1686
+ } else if (e.key === 'Escape') {
1687
+ renderMockList(currentMocks, true);
1688
+ }
1689
+ });
1690
+
1691
+ li.appendChild(input);
1692
+ input.focus();
1693
+ input.select();
1694
+ }
1695
+
1696
+ async function enableAllMocks() {
1697
+ await batchToggleMockEnable(currentMocks.map(m => m.key), true);
1698
+ currentMocks = await fetchMocks();
1699
+ renderMockList(currentMocks, true);
1700
+ }
1701
+
1702
+ async function disableAllMocks() {
1703
+ await batchToggleMockEnable(currentMocks.map(m => m.key), false);
1704
+ currentMocks = await fetchMocks();
1705
+ renderMockList(currentMocks, true);
1706
+ }
1707
+
1708
+ // Initialize sidebar resizer functionality
1709
+ function initSidebarResizer() {
1710
+ const sidebar = document.getElementById('sidebar');
1711
+ const resizer = document.getElementById('resizer');
1712
+ const main = document.querySelector('main');
1713
+
1714
+ if (!sidebar || !resizer || !main) {
1715
+ console.warn('[Inspector] Sidebar resizer elements not found');
1716
+ return;
1717
+ }
1718
+
1719
+ // Load saved width from localStorage
1720
+ const savedWidth = localStorage.getItem('mockInspectorSidebarWidth');
1721
+ if (savedWidth) {
1722
+ const width = Math.max(200, Math.min(800, parseInt(savedWidth, 10)));
1723
+ main.style.setProperty('--sidebar-width', width + 'px');
1724
+ }
1725
+
1726
+ let isResizing = false;
1727
+ let startX = 0;
1728
+ let startWidth = 0;
1729
+
1730
+ resizer.addEventListener('mousedown', (e) => {
1731
+ isResizing = true;
1732
+ startX = e.clientX;
1733
+ startWidth = sidebar.offsetWidth;
1734
+ resizer.classList.add('active');
1735
+ document.body.style.cursor = 'col-resize';
1736
+ document.body.style.userSelect = 'none';
1737
+ e.preventDefault();
1738
+ });
1739
+
1740
+ document.addEventListener('mousemove', (e) => {
1741
+ if (!isResizing) return;
1742
+
1743
+ const deltaX = e.clientX - startX;
1744
+ const newWidth = Math.max(200, Math.min(800, startWidth + deltaX));
1745
+ main.style.setProperty('--sidebar-width', newWidth + 'px');
1746
+ });
1747
+
1748
+ document.addEventListener('mouseup', () => {
1749
+ if (isResizing) {
1750
+ isResizing = false;
1751
+ resizer.classList.remove('active');
1752
+ document.body.style.cursor = '';
1753
+ document.body.style.userSelect = '';
1754
+
1755
+ // Save width to localStorage
1756
+ const currentWidth = sidebar.offsetWidth;
1757
+ localStorage.setItem('mockInspectorSidebarWidth', currentWidth.toString());
1758
+ }
1759
+ });
1760
+
1761
+ // Touch support for mobile devices
1762
+ resizer.addEventListener('touchstart', (e) => {
1763
+ const touch = e.touches[0];
1764
+ isResizing = true;
1765
+ startX = touch.clientX;
1766
+ startWidth = sidebar.offsetWidth;
1767
+ resizer.classList.add('active');
1768
+ e.preventDefault();
1769
+ }, { passive: false });
1770
+
1771
+ document.addEventListener('touchmove', (e) => {
1772
+ if (!isResizing) return;
1773
+
1774
+ const touch = e.touches[0];
1775
+ const deltaX = touch.clientX - startX;
1776
+ const newWidth = Math.max(200, Math.min(800, startWidth + deltaX));
1777
+ main.style.setProperty('--sidebar-width', newWidth + 'px');
1778
+ }, { passive: false });
1779
+
1780
+ document.addEventListener('touchend', () => {
1781
+ if (isResizing) {
1782
+ isResizing = false;
1783
+ resizer.classList.remove('active');
1784
+
1785
+ // Save width to localStorage
1786
+ const currentWidth = sidebar.offsetWidth;
1787
+ localStorage.setItem('mockInspectorSidebarWidth', currentWidth.toString());
1788
+ }
1789
+ });
1790
+ }
1791
+
1792
+ async function bootstrap() {
1793
+ // Initialize sidebar resizer
1794
+ initSidebarResizer();
1795
+
1796
+ try {
1797
+ currentMocks = await fetchMocks();
1798
+ renderMockList(currentMocks, true);
1799
+
1800
+ // 绑定全局控制按钮
1801
+ const enableAllBtn = document.getElementById('enable-all');
1802
+ const disableAllBtn = document.getElementById('disable-all');
1803
+ if (enableAllBtn) {
1804
+ enableAllBtn.addEventListener('click', async () => {
1805
+ enableAllBtn.disabled = true;
1806
+ enableAllBtn.textContent = '处理中...';
1807
+ await enableAllMocks();
1808
+ enableAllBtn.disabled = false;
1809
+ enableAllBtn.textContent = '✓ 开启所有';
1810
+ });
1811
+ }
1812
+ if (disableAllBtn) {
1813
+ disableAllBtn.addEventListener('click', async () => {
1814
+ disableAllBtn.disabled = true;
1815
+ disableAllBtn.textContent = '处理中...';
1816
+ await disableAllMocks();
1817
+ disableAllBtn.disabled = false;
1818
+ disableAllBtn.textContent = '✗ 关闭所有';
1819
+ });
1820
+ }
1821
+
1822
+ // 绑定新建API按钮
1823
+ const newApiBtn = document.getElementById('new-api-btn');
1824
+ const newApiModal = document.getElementById('new-api-modal');
1825
+ const newApiForm = document.getElementById('new-api-form');
1826
+ const cancelNewApi = document.getElementById('cancel-new-api');
1827
+
1828
+ if (newApiBtn && newApiModal) {
1829
+ newApiBtn.addEventListener('click', () => {
1830
+ newApiModal.classList.add('show');
1831
+ document.getElementById('new-api-path').focus();
1832
+ });
1833
+ }
1834
+
1835
+ if (cancelNewApi && newApiModal) {
1836
+ cancelNewApi.addEventListener('click', () => {
1837
+ newApiModal.classList.remove('show');
1838
+ newApiForm.reset();
1839
+ });
1840
+ }
1841
+
1842
+ // 点击背景关闭模态框
1843
+ if (newApiModal) {
1844
+ newApiModal.addEventListener('click', (e) => {
1845
+ if (e.target === newApiModal) {
1846
+ newApiModal.classList.remove('show');
1847
+ newApiForm.reset();
1848
+ }
1849
+ });
1850
+ }
1851
+
1852
+ // 处理表单提交
1853
+ if (newApiForm) {
1854
+ newApiForm.addEventListener('submit', async (e) => {
1855
+ e.preventDefault();
1856
+
1857
+ const method = document.getElementById('new-api-method').value;
1858
+ const path = document.getElementById('new-api-path').value.trim();
1859
+ const description = document.getElementById('new-api-description').value.trim();
1860
+ const dataText = document.getElementById('new-api-data').value.trim();
1861
+
1862
+ if (!path) {
1863
+ alert('请输入 API 路径');
1864
+ return;
1865
+ }
1866
+
1867
+ // 验证JSON
1868
+ let data;
1869
+ try {
1870
+ data = JSON.parse(dataText || '{}');
1871
+ } catch (err) {
1872
+ alert('Response Data 不是有效的 JSON: ' + err.message);
1873
+ return;
1874
+ }
1875
+
1876
+ // 发送创建请求
1877
+ try {
1878
+ const submitBtn = newApiForm.querySelector('button[type="submit"]');
1879
+ const originalText = submitBtn.textContent;
1880
+ submitBtn.disabled = true;
1881
+ submitBtn.textContent = 'Creating...';
1882
+
1883
+ const res = await fetch(apiBase + '/create', {
1884
+ method: 'POST',
1885
+ headers: { 'Content-Type': 'application/json' },
1886
+ body: JSON.stringify({
1887
+ method: method,
1888
+ path: path,
1889
+ description: description || path,
1890
+ data: data
1891
+ })
1892
+ });
1893
+
1894
+ const result = await res.json();
1895
+
1896
+ if (!res.ok) {
1897
+ throw new Error(result.error || 'Failed to create API');
1898
+ }
1899
+
1900
+ // 关闭模态框
1901
+ newApiModal.classList.remove('show');
1902
+ newApiForm.reset();
1903
+
1904
+ // 刷新列表
1905
+ currentMocks = await fetchMocks();
1906
+ renderMockList(currentMocks, true);
1907
+
1908
+ // 自动选中新创建的 API
1909
+ if (result.mock && result.mock.key) {
1910
+ const button = document.querySelector('aside button[data-key="' + result.mock.key + '"]');
1911
+ if (button) {
1912
+ await selectMock(result.mock.key, button);
1913
+ }
1914
+ }
1915
+
1916
+ alert('✅ API Mock 创建成功!');
1917
+
1918
+ submitBtn.disabled = false;
1919
+ submitBtn.textContent = originalText;
1920
+ } catch (err) {
1921
+ alert('创建失败: ' + err.message);
1922
+ const submitBtn = newApiForm.querySelector('button[type="submit"]');
1923
+ submitBtn.disabled = false;
1924
+ submitBtn.textContent = 'Create';
1925
+ }
1926
+ });
1927
+ }
1928
+ } catch (error) {
1929
+ console.error('[Inspector] Bootstrap failed:', error);
1930
+ }
1931
+ }
1932
+
1933
+ bootstrap();
1934
+ </script>
1935
+
1936
+ <!-- Delete Confirmation Modal -->
1937
+ <div id="delete-confirm-modal" class="modal-overlay">
1938
+ <div class="modal" style="max-width: 400px;">
1939
+ <h2><span class="btn-icon-cross" style="color: var(--accent-rose);">!</span> 确认删除</h2>
1940
+ <p id="delete-confirm-message" style="margin-bottom: 1rem; color: var(--text-secondary);"></p>
1941
+ <label style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; cursor: pointer; user-select: none;">
1942
+ <input type="checkbox" id="delete-never-ask" style="flex-shrink: 0;" />
1943
+ <span>不再提醒</span>
1944
+ </label>
1945
+ <div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
1946
+ <button id="delete-cancel-btn" class="secondary">取消</button>
1947
+ <button id="delete-confirm-btn" class="primary" style="background: var(--accent-rose);">删除</button>
1948
+ </div>
1949
+ </div>
1950
+ </div>
1951
+ </body>