mock-service-cli 4.3.0 → 4.3.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.
- package/README.md +0 -1
- package/dist/file-explorer.html +387 -71
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
[](https://github.com/chandq/mock-service-cli/actions/workflows/release.yml)
|
|
2
2
|
[](https://coveralls.io/github/chandq/mock-service-cli?branch=master)
|
|
3
3
|
[](https://www.npmjs.com/package/mock-service-cli)
|
|
4
|
-
[](https://nodejs.org/download/release/v12.0.0/)
|
|
5
4
|
[](https://github.com/chandq/mock-service-cli/blob/master/LICENSE.md)
|
|
6
5
|
|
|
7
6
|
# 🦅 mock-service-cli
|
package/dist/file-explorer.html
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
box-sizing: border-box;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.icon-sprite {
|
|
15
|
+
position: absolute;
|
|
16
|
+
width: 0;
|
|
17
|
+
height: 0;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
body {
|
|
15
22
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
16
23
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
@@ -335,6 +342,7 @@
|
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
.file-grid {
|
|
345
|
+
--list-row-height: 37px;
|
|
338
346
|
position: relative;
|
|
339
347
|
overflow: auto;
|
|
340
348
|
flex: 1;
|
|
@@ -351,19 +359,21 @@
|
|
|
351
359
|
top: 0;
|
|
352
360
|
left: 0;
|
|
353
361
|
right: 0;
|
|
362
|
+
overflow-anchor: none;
|
|
354
363
|
}
|
|
355
364
|
|
|
356
365
|
.virtual-content.grid-view {
|
|
357
|
-
display:
|
|
358
|
-
gap: 8px;
|
|
359
|
-
grid-auto-rows: 124px;
|
|
360
|
-
padding: 8px;
|
|
366
|
+
display: block;
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
.virtual-content.list-view {
|
|
364
370
|
display: block;
|
|
365
371
|
}
|
|
366
372
|
|
|
373
|
+
.virtual-content .file-item {
|
|
374
|
+
position: absolute;
|
|
375
|
+
}
|
|
376
|
+
|
|
367
377
|
.file-item {
|
|
368
378
|
display: flex;
|
|
369
379
|
flex-direction: column;
|
|
@@ -371,7 +381,7 @@
|
|
|
371
381
|
padding: 10px 8px;
|
|
372
382
|
border-radius: 6px;
|
|
373
383
|
cursor: pointer;
|
|
374
|
-
transition:
|
|
384
|
+
transition: background-color 0.2s;
|
|
375
385
|
}
|
|
376
386
|
|
|
377
387
|
.grid-view .file-item {
|
|
@@ -393,7 +403,7 @@
|
|
|
393
403
|
border-bottom: 1px solid #f0f0f0;
|
|
394
404
|
border-radius: 0;
|
|
395
405
|
min-height: auto;
|
|
396
|
-
height:
|
|
406
|
+
height: var(--list-row-height);
|
|
397
407
|
}
|
|
398
408
|
|
|
399
409
|
.file-icon {
|
|
@@ -460,6 +470,7 @@
|
|
|
460
470
|
margin-bottom: 0;
|
|
461
471
|
flex: 1;
|
|
462
472
|
font-size: 12px;
|
|
473
|
+
min-width: 0;
|
|
463
474
|
}
|
|
464
475
|
|
|
465
476
|
.file-meta {
|
|
@@ -468,6 +479,11 @@
|
|
|
468
479
|
}
|
|
469
480
|
.birthtime {
|
|
470
481
|
padding-right: 4px;
|
|
482
|
+
display: none;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.birthtime.visible {
|
|
486
|
+
display: inline-block;
|
|
471
487
|
}
|
|
472
488
|
|
|
473
489
|
.list-view .file-meta {
|
|
@@ -476,6 +492,11 @@
|
|
|
476
492
|
font-size: 10px;
|
|
477
493
|
}
|
|
478
494
|
|
|
495
|
+
.date-compact,
|
|
496
|
+
.more-actions-btn {
|
|
497
|
+
display: none;
|
|
498
|
+
}
|
|
499
|
+
|
|
479
500
|
.hidden-file {
|
|
480
501
|
opacity: 0.6;
|
|
481
502
|
}
|
|
@@ -486,7 +507,50 @@
|
|
|
486
507
|
}
|
|
487
508
|
|
|
488
509
|
.list-view .action-buttons {
|
|
489
|
-
display:
|
|
510
|
+
display: none;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.list-view .more-actions-btn {
|
|
514
|
+
display: inline-flex;
|
|
515
|
+
flex: 0 0 32px;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: center;
|
|
518
|
+
align-self: center;
|
|
519
|
+
width: 32px;
|
|
520
|
+
height: 28px;
|
|
521
|
+
margin-left: 8px;
|
|
522
|
+
padding: 0;
|
|
523
|
+
border: 1px solid transparent;
|
|
524
|
+
border-radius: 5px;
|
|
525
|
+
background: #f1f3f5;
|
|
526
|
+
color: #555;
|
|
527
|
+
font-size: 20px;
|
|
528
|
+
line-height: 1;
|
|
529
|
+
cursor: pointer;
|
|
530
|
+
touch-action: manipulation;
|
|
531
|
+
-webkit-tap-highlight-color: transparent;
|
|
532
|
+
transition:
|
|
533
|
+
background-color 120ms ease,
|
|
534
|
+
border-color 120ms ease,
|
|
535
|
+
color 120ms ease,
|
|
536
|
+
transform 120ms ease;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.list-view .more-actions-btn:hover {
|
|
540
|
+
border-color: #d9dce1;
|
|
541
|
+
background: #e5e7eb;
|
|
542
|
+
color: #222;
|
|
543
|
+
transform: scale(1.06);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.list-view .more-actions-btn:active {
|
|
547
|
+
background: #d9dce1;
|
|
548
|
+
transform: scale(0.92);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.list-view .more-actions-btn:focus-visible {
|
|
552
|
+
outline: 2px solid #667eea;
|
|
553
|
+
outline-offset: 1px;
|
|
490
554
|
}
|
|
491
555
|
|
|
492
556
|
.action-btn {
|
|
@@ -523,6 +587,90 @@
|
|
|
523
587
|
color: white;
|
|
524
588
|
}
|
|
525
589
|
|
|
590
|
+
@media (max-width: 767px) {
|
|
591
|
+
.file-grid.list-view {
|
|
592
|
+
--list-row-height: 48px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.file-grid.list-view.show-birthtime {
|
|
596
|
+
--list-row-height: 65px;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.list-view .file-item {
|
|
600
|
+
align-items: flex-start;
|
|
601
|
+
padding: 7px 10px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.list-view .file-icon {
|
|
605
|
+
flex: 0 0 20px;
|
|
606
|
+
margin-top: 2px;
|
|
607
|
+
margin-right: 7px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.list-view .file-select {
|
|
611
|
+
flex: 0 0 auto;
|
|
612
|
+
margin: 3px 7px 0 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.list-view .file-info {
|
|
616
|
+
display: flex;
|
|
617
|
+
flex: 1;
|
|
618
|
+
flex-direction: column;
|
|
619
|
+
align-items: stretch;
|
|
620
|
+
gap: 3px;
|
|
621
|
+
min-width: 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.list-view .file-name {
|
|
625
|
+
width: 100%;
|
|
626
|
+
overflow: hidden;
|
|
627
|
+
text-overflow: ellipsis;
|
|
628
|
+
white-space: nowrap;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.list-view .file-meta {
|
|
632
|
+
display: flex;
|
|
633
|
+
width: 100%;
|
|
634
|
+
gap: 3px;
|
|
635
|
+
flex-wrap: wrap;
|
|
636
|
+
overflow: hidden;
|
|
637
|
+
white-space: nowrap;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.list-view .file-meta > span {
|
|
641
|
+
overflow: hidden;
|
|
642
|
+
text-overflow: ellipsis;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.date-full {
|
|
646
|
+
display: none;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.date-compact {
|
|
650
|
+
display: inline;
|
|
651
|
+
vertical-align: middle;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.list-view .birthtime.visible {
|
|
655
|
+
flex-basis: 100%;
|
|
656
|
+
display: block;
|
|
657
|
+
padding-right: 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.list-view .action-buttons {
|
|
661
|
+
display: none !important;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.list-view .more-actions-btn {
|
|
665
|
+
align-self: flex-start;
|
|
666
|
+
margin: 10px 0 0 6px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.file-grid.list-view.show-birthtime .more-actions-btn {
|
|
670
|
+
margin-top: 17px;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
526
674
|
.modal {
|
|
527
675
|
display: none;
|
|
528
676
|
position: fixed;
|
|
@@ -828,6 +976,15 @@
|
|
|
828
976
|
</style>
|
|
829
977
|
</head>
|
|
830
978
|
<body>
|
|
979
|
+
<svg class="icon-sprite" aria-hidden="true" focusable="false">
|
|
980
|
+
<symbol id="icon-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
981
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
982
|
+
</symbol>
|
|
983
|
+
<symbol id="icon-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
984
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
985
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
986
|
+
</symbol>
|
|
987
|
+
</svg>
|
|
831
988
|
<div class="container">
|
|
832
989
|
<div class="header">
|
|
833
990
|
<h1>📁 文件浏览器</h1>
|
|
@@ -1047,6 +1204,12 @@
|
|
|
1047
1204
|
case 'toggleSelect':
|
|
1048
1205
|
toggleSelectedPath(path, actionBtn.checked, actionBtn.closest('.file-item'));
|
|
1049
1206
|
break;
|
|
1207
|
+
case 'more': {
|
|
1208
|
+
const fileItem = actionBtn.closest('.file-item');
|
|
1209
|
+
const rect = actionBtn.getBoundingClientRect();
|
|
1210
|
+
showContextMenu(rect.right, rect.bottom, fileItem.dataset.path, fileItem.dataset.isDirectory === 'true');
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1050
1213
|
}
|
|
1051
1214
|
return;
|
|
1052
1215
|
}
|
|
@@ -1139,7 +1302,7 @@
|
|
|
1139
1302
|
addEventListenerWithCleanup(document, 'keydown', handleDocumentKeydown);
|
|
1140
1303
|
|
|
1141
1304
|
fileGridResizeObserver = new ResizeObserver(() => {
|
|
1142
|
-
if (virtualState
|
|
1305
|
+
if (virtualState) {
|
|
1143
1306
|
renderFiles(currentFiles, false);
|
|
1144
1307
|
}
|
|
1145
1308
|
});
|
|
@@ -1600,10 +1763,12 @@
|
|
|
1600
1763
|
|
|
1601
1764
|
if (currentView === 'list') {
|
|
1602
1765
|
fileGrid.classList.add('list-view');
|
|
1766
|
+
fileGrid.classList.toggle('show-birthtime', showBirthtime);
|
|
1603
1767
|
toggleBirthtimeBtn.style.display = 'inline-block';
|
|
1604
1768
|
} else {
|
|
1605
1769
|
toggleBirthtimeBtn.style.display = 'none';
|
|
1606
1770
|
fileGrid.classList.remove('list-view');
|
|
1771
|
+
fileGrid.classList.remove('show-birthtime');
|
|
1607
1772
|
}
|
|
1608
1773
|
|
|
1609
1774
|
if (!files || files.length === 0) {
|
|
@@ -1615,19 +1780,33 @@
|
|
|
1615
1780
|
const isListView = currentView === 'list';
|
|
1616
1781
|
const columnCount = isListView
|
|
1617
1782
|
? 1
|
|
1618
|
-
: Math.max(
|
|
1619
|
-
|
|
1783
|
+
: Math.max(
|
|
1784
|
+
1,
|
|
1785
|
+
Math.floor((Math.max(fileGrid.clientWidth, GRID_MIN_ITEM_WIDTH + 16) - 8) / (GRID_MIN_ITEM_WIDTH + 8))
|
|
1786
|
+
);
|
|
1787
|
+
const rowHeight = isListView ? getListRowHeight(fileGrid) : GRID_ROW_HEIGHT;
|
|
1620
1788
|
const totalRows = Math.ceil(files.length / columnCount);
|
|
1789
|
+
const gridItemWidth = isListView ? null : (fileGrid.clientWidth - 16 - (columnCount - 1) * 8) / columnCount;
|
|
1621
1790
|
const spacer = document.createElement('div');
|
|
1622
1791
|
spacer.className = 'virtual-spacer';
|
|
1623
1792
|
spacer.style.height = `${totalRows * rowHeight + (isListView ? 0 : 8)}px`;
|
|
1624
1793
|
const content = document.createElement('div');
|
|
1625
1794
|
content.className = `virtual-content ${isListView ? 'list-view' : 'grid-view'}`;
|
|
1626
|
-
if (!isListView) {
|
|
1627
|
-
content.style.gridTemplateColumns = `repeat(${columnCount}, minmax(${GRID_MIN_ITEM_WIDTH}px, 1fr))`;
|
|
1628
|
-
}
|
|
1629
1795
|
|
|
1630
|
-
virtualState = {
|
|
1796
|
+
virtualState = {
|
|
1797
|
+
files,
|
|
1798
|
+
columnCount,
|
|
1799
|
+
rowHeight,
|
|
1800
|
+
gridItemWidth,
|
|
1801
|
+
isListView,
|
|
1802
|
+
spacer,
|
|
1803
|
+
content,
|
|
1804
|
+
itemPool: [],
|
|
1805
|
+
freeItems: [],
|
|
1806
|
+
renderedItems: new Map(),
|
|
1807
|
+
startRow: -1,
|
|
1808
|
+
endRow: -1
|
|
1809
|
+
};
|
|
1631
1810
|
fileGrid.replaceChildren(spacer, content);
|
|
1632
1811
|
if (resetScroll) fileGrid.scrollTop = 0;
|
|
1633
1812
|
renderVirtualWindow();
|
|
@@ -1641,67 +1820,195 @@
|
|
|
1641
1820
|
});
|
|
1642
1821
|
}
|
|
1643
1822
|
|
|
1823
|
+
function getListRowHeight(fileGrid) {
|
|
1824
|
+
const rowHeight = Number.parseFloat(getComputedStyle(fileGrid).getPropertyValue('--list-row-height'));
|
|
1825
|
+
return Number.isFinite(rowHeight) ? rowHeight : LIST_ROW_HEIGHT;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1644
1828
|
function renderVirtualWindow() {
|
|
1645
1829
|
if (!virtualState) return;
|
|
1646
1830
|
const fileGrid = document.getElementById('fileGrid');
|
|
1647
|
-
const {
|
|
1831
|
+
const {
|
|
1832
|
+
files,
|
|
1833
|
+
columnCount,
|
|
1834
|
+
rowHeight,
|
|
1835
|
+
gridItemWidth,
|
|
1836
|
+
isListView,
|
|
1837
|
+
content,
|
|
1838
|
+
itemPool,
|
|
1839
|
+
freeItems,
|
|
1840
|
+
renderedItems
|
|
1841
|
+
} = virtualState;
|
|
1648
1842
|
const firstVisibleRow = Math.floor(fileGrid.scrollTop / rowHeight);
|
|
1649
1843
|
const visibleRows = Math.ceil(fileGrid.clientHeight / rowHeight);
|
|
1650
1844
|
const startRow = Math.max(0, firstVisibleRow - VIRTUAL_BUFFER_ROWS);
|
|
1651
|
-
const endRow = Math.min(
|
|
1845
|
+
const endRow = Math.min(
|
|
1846
|
+
Math.ceil(files.length / columnCount),
|
|
1847
|
+
firstVisibleRow + visibleRows + VIRTUAL_BUFFER_ROWS
|
|
1848
|
+
);
|
|
1652
1849
|
const startIndex = startRow * columnCount;
|
|
1653
1850
|
const endIndex = Math.min(files.length, endRow * columnCount);
|
|
1654
1851
|
|
|
1655
1852
|
if (virtualState.startRow === startRow && virtualState.endRow === endRow) return;
|
|
1656
1853
|
virtualState.startRow = startRow;
|
|
1657
1854
|
virtualState.endRow = endRow;
|
|
1658
|
-
|
|
1855
|
+
renderedItems.forEach((fileItem, fileIndex) => {
|
|
1856
|
+
if (fileIndex < startIndex || fileIndex >= endIndex) {
|
|
1857
|
+
renderedItems.delete(fileIndex);
|
|
1858
|
+
fileItem.hidden = true;
|
|
1859
|
+
freeItems.push(fileItem);
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
for (let fileIndex = startIndex; fileIndex < endIndex; fileIndex++) {
|
|
1864
|
+
if (renderedItems.has(fileIndex)) continue;
|
|
1865
|
+
|
|
1866
|
+
let fileItem = freeItems.pop();
|
|
1867
|
+
if (!fileItem) {
|
|
1868
|
+
fileItem = createFileItemElement(isListView);
|
|
1869
|
+
itemPool.push(fileItem);
|
|
1870
|
+
content.appendChild(fileItem);
|
|
1871
|
+
}
|
|
1872
|
+
updateFileItem(fileItem, files[fileIndex]);
|
|
1873
|
+
positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView);
|
|
1874
|
+
fileItem.hidden = false;
|
|
1875
|
+
renderedItems.set(fileIndex, fileItem);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1659
1878
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1879
|
+
function positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView) {
|
|
1880
|
+
fileItem.style.transform = '';
|
|
1881
|
+
if (isListView) {
|
|
1882
|
+
fileItem.style.left = '0';
|
|
1883
|
+
fileItem.style.right = '0';
|
|
1884
|
+
fileItem.style.top = `${fileIndex * rowHeight}px`;
|
|
1885
|
+
fileItem.style.width = '';
|
|
1886
|
+
return;
|
|
1662
1887
|
}
|
|
1663
1888
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1889
|
+
const row = Math.floor(fileIndex / columnCount);
|
|
1890
|
+
const column = fileIndex % columnCount;
|
|
1891
|
+
fileItem.style.left = `${8 + column * (gridItemWidth + 8)}px`;
|
|
1892
|
+
fileItem.style.right = 'auto';
|
|
1893
|
+
fileItem.style.top = `${8 + row * rowHeight}px`;
|
|
1894
|
+
fileItem.style.width = `${gridItemWidth}px`;
|
|
1666
1895
|
}
|
|
1667
1896
|
|
|
1668
|
-
function
|
|
1669
|
-
const icon = getFileIcon(file);
|
|
1670
|
-
const size = formatSize(file.size);
|
|
1671
|
-
const mtime = formatDate(file.mtime);
|
|
1672
|
-
const birthtime = formatDate(file.birthtime);
|
|
1673
|
-
const hiddenClass = file.isHidden ? 'hidden-file' : '';
|
|
1674
|
-
const selectedClass = selectedPaths.has(file.path) ? 'selected' : '';
|
|
1675
|
-
const selectedAttr = selectedPaths.has(file.path) ? 'checked' : '';
|
|
1676
|
-
const selectLabel = file.isDirectory ? '选择文件夹' : '选择文件';
|
|
1677
|
-
const pathAttr = escapeAttribute(file.path);
|
|
1678
|
-
const nameAttr = escapeAttribute(file.name);
|
|
1897
|
+
function createFileItemElement(isListView) {
|
|
1679
1898
|
const fileItem = document.createElement('div');
|
|
1680
|
-
fileItem.className =
|
|
1681
|
-
|
|
1682
|
-
|
|
1899
|
+
fileItem.className = 'file-item';
|
|
1900
|
+
const select = document.createElement('input');
|
|
1901
|
+
select.className = 'file-select edit-only edit-inline-block';
|
|
1902
|
+
select.type = 'checkbox';
|
|
1903
|
+
select.dataset.action = 'toggleSelect';
|
|
1904
|
+
fileItem.appendChild(select);
|
|
1905
|
+
|
|
1906
|
+
const icon = createFileIconElement();
|
|
1907
|
+
fileItem.appendChild(icon.wrapper);
|
|
1908
|
+
|
|
1909
|
+
const info = document.createElement('div');
|
|
1910
|
+
info.className = 'file-info';
|
|
1911
|
+
const name = document.createElement('div');
|
|
1912
|
+
name.className = 'file-name';
|
|
1913
|
+
info.appendChild(name);
|
|
1914
|
+
const meta = document.createElement('div');
|
|
1915
|
+
meta.className = 'file-meta';
|
|
1916
|
+
const type = document.createElement('span');
|
|
1917
|
+
meta.appendChild(type);
|
|
1918
|
+
info.appendChild(meta);
|
|
1919
|
+
fileItem.appendChild(info);
|
|
1920
|
+
|
|
1921
|
+
const refs = { select, icon, name, type };
|
|
1683
1922
|
|
|
1684
1923
|
if (isListView) {
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
${!file.isDirectory ? `<button class="action-btn download" data-action="download" data-path="${pathAttr}" data-filename="${nameAttr}" title="下载">⬇️</button>` : ''}
|
|
1693
|
-
<button class="action-btn rename edit-only edit-inline" data-action="rename" data-path="${pathAttr}" data-filename="${nameAttr}" title="重命名">✏️</button>
|
|
1694
|
-
<button class="action-btn delete edit-only edit-inline" data-action="delete" data-path="${pathAttr}" data-filename="${nameAttr}" title="删除">🗑️</button>
|
|
1695
|
-
</div>`;
|
|
1696
|
-
} else {
|
|
1697
|
-
fileItem.innerHTML = `
|
|
1698
|
-
<input class="file-select edit-only edit-inline-block" type="checkbox" data-action="toggleSelect" data-path="${pathAttr}" title="${selectLabel}" ${selectedAttr} />
|
|
1699
|
-
<div class="file-icon">${icon}</div>
|
|
1700
|
-
<div class="file-info"><div class="file-name">${escapeHtml(file.name)}</div><div class="file-meta">${file.isDirectory ? '目录' : size}</div></div>`;
|
|
1924
|
+
const mtime = createDateMeta();
|
|
1925
|
+
const birthtime = createDateMeta('birthtime');
|
|
1926
|
+
meta.append(mtime.container, birthtime.container);
|
|
1927
|
+
const more = createActionButton('more-actions-btn', 'more', '更多操作', '⋮');
|
|
1928
|
+
more.setAttribute('aria-label', '更多操作');
|
|
1929
|
+
fileItem.appendChild(more);
|
|
1930
|
+
Object.assign(refs, { mtime, birthtime });
|
|
1701
1931
|
}
|
|
1932
|
+
|
|
1933
|
+
fileItem._refs = refs;
|
|
1702
1934
|
return fileItem;
|
|
1703
1935
|
}
|
|
1704
1936
|
|
|
1937
|
+
function createFileIconElement() {
|
|
1938
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
1939
|
+
const wrapper = document.createElement('div');
|
|
1940
|
+
wrapper.className = 'file-icon';
|
|
1941
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
1942
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1943
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
1944
|
+
const use = document.createElementNS(namespace, 'use');
|
|
1945
|
+
svg.appendChild(use);
|
|
1946
|
+
wrapper.appendChild(svg);
|
|
1947
|
+
return { wrapper, svg, use };
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function createDateMeta(className = '') {
|
|
1951
|
+
const container = document.createElement('span');
|
|
1952
|
+
if (className) container.className = className;
|
|
1953
|
+
const full = document.createElement('span');
|
|
1954
|
+
full.className = 'date-full';
|
|
1955
|
+
const compact = document.createElement('span');
|
|
1956
|
+
compact.className = 'date-compact';
|
|
1957
|
+
container.append(full, compact);
|
|
1958
|
+
return { container, full, compact };
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function createActionButton(className, action, title, label) {
|
|
1962
|
+
const button = document.createElement('button');
|
|
1963
|
+
button.className = className;
|
|
1964
|
+
button.dataset.action = action;
|
|
1965
|
+
button.title = title;
|
|
1966
|
+
button.textContent = label;
|
|
1967
|
+
return button;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function updateFileItem(fileItem, file) {
|
|
1971
|
+
const refs = fileItem._refs;
|
|
1972
|
+
const view = getFileView(file);
|
|
1973
|
+
const isSelected = selectedPaths.has(file.path);
|
|
1974
|
+
fileItem.className = `file-item${file.isHidden ? ' hidden-file' : ''}${isSelected ? ' selected' : ''}`;
|
|
1975
|
+
fileItem.dataset.path = file.path;
|
|
1976
|
+
fileItem.dataset.isDirectory = file.isDirectory;
|
|
1977
|
+
refs.select.dataset.path = file.path;
|
|
1978
|
+
refs.select.checked = isSelected;
|
|
1979
|
+
refs.select.title = file.isDirectory ? '选择文件夹' : '选择文件';
|
|
1980
|
+
updateFileIcon(refs.icon, view.iconInfo);
|
|
1981
|
+
refs.name.textContent = file.name;
|
|
1982
|
+
refs.type.textContent = view.typeLabel;
|
|
1983
|
+
|
|
1984
|
+
if (!refs.mtime) return;
|
|
1985
|
+
|
|
1986
|
+
refs.mtime.full.textContent = view.mtime;
|
|
1987
|
+
refs.mtime.compact.textContent = view.compactMtime;
|
|
1988
|
+
refs.birthtime.full.textContent = view.birthtime;
|
|
1989
|
+
refs.birthtime.compact.textContent = view.compactBirthtime;
|
|
1990
|
+
refs.birthtime.container.classList.toggle('visible', showBirthtime);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function getFileView(file) {
|
|
1994
|
+
if (!file._view) {
|
|
1995
|
+
file._view = {
|
|
1996
|
+
typeLabel: file.isDirectory ? '目录' : formatSize(file.size),
|
|
1997
|
+
mtime: formatDate(file.mtime),
|
|
1998
|
+
compactMtime: formatDate(file.mtime, false),
|
|
1999
|
+
birthtime: formatDate(file.birthtime),
|
|
2000
|
+
compactBirthtime: `创建 ${formatDate(file.birthtime, false)}`,
|
|
2001
|
+
iconInfo: getFileIconInfo(file)
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
return file._view;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function updateFileIcon(icon, iconInfo) {
|
|
2008
|
+
icon.use.setAttribute('href', iconInfo.symbol);
|
|
2009
|
+
icon.svg.style.color = iconInfo.color;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1705
2012
|
function handleItemClick(path, isDirectory) {
|
|
1706
2013
|
if (isDirectory) {
|
|
1707
2014
|
loadDirectory(path);
|
|
@@ -2178,14 +2485,16 @@
|
|
|
2178
2485
|
renderFiles(currentFiles);
|
|
2179
2486
|
}
|
|
2180
2487
|
|
|
2181
|
-
|
|
2488
|
+
const fileIconInfoCache = new Map();
|
|
2489
|
+
|
|
2490
|
+
function getFileIconInfo(file) {
|
|
2182
2491
|
if (file.isDirectory) {
|
|
2183
|
-
return
|
|
2184
|
-
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
2185
|
-
</svg>`;
|
|
2492
|
+
return { symbol: '#icon-folder', color: '#667eea' };
|
|
2186
2493
|
}
|
|
2187
2494
|
|
|
2188
2495
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
2496
|
+
const cached = fileIconInfoCache.get(ext);
|
|
2497
|
+
if (cached) return cached;
|
|
2189
2498
|
const iconColors = {
|
|
2190
2499
|
js: '#f7df1e',
|
|
2191
2500
|
json: '#292929',
|
|
@@ -2263,12 +2572,9 @@
|
|
|
2263
2572
|
webm: '#2196f3'
|
|
2264
2573
|
};
|
|
2265
2574
|
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
return
|
|
2269
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
2270
|
-
<polyline points="14 2 14 8 20 8"></polyline>
|
|
2271
|
-
</svg>`;
|
|
2575
|
+
const iconInfo = { symbol: '#icon-file', color: iconColors[ext] || '#95a5a6' };
|
|
2576
|
+
fileIconInfoCache.set(ext, iconInfo);
|
|
2577
|
+
return iconInfo;
|
|
2272
2578
|
}
|
|
2273
2579
|
|
|
2274
2580
|
function formatSize(bytes) {
|
|
@@ -2279,9 +2585,15 @@
|
|
|
2279
2585
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
2280
2586
|
}
|
|
2281
2587
|
|
|
2282
|
-
function formatDate(
|
|
2283
|
-
const date = new Date(
|
|
2284
|
-
|
|
2588
|
+
function formatDate(value, includeSeconds = true) {
|
|
2589
|
+
const date = new Date(value);
|
|
2590
|
+
const year = date.getFullYear();
|
|
2591
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2592
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
2593
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
2594
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
2595
|
+
const base = `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
2596
|
+
return includeSeconds ? `${base}:${String(date.getSeconds()).padStart(2, '0')}` : base;
|
|
2285
2597
|
}
|
|
2286
2598
|
|
|
2287
2599
|
function escapeHtml(text) {
|
|
@@ -2291,13 +2603,17 @@
|
|
|
2291
2603
|
}
|
|
2292
2604
|
|
|
2293
2605
|
function escapeAttribute(value) {
|
|
2294
|
-
return String(value).replace(
|
|
2295
|
-
'
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2606
|
+
return String(value).replace(
|
|
2607
|
+
/[&<>"']/g,
|
|
2608
|
+
char =>
|
|
2609
|
+
({
|
|
2610
|
+
'&': '&',
|
|
2611
|
+
'<': '<',
|
|
2612
|
+
'>': '>',
|
|
2613
|
+
'"': '"',
|
|
2614
|
+
"'": '''
|
|
2615
|
+
})[char]
|
|
2616
|
+
);
|
|
2301
2617
|
}
|
|
2302
2618
|
</script>
|
|
2303
2619
|
</body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mock-service-cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "🦅 Local Mock/Static/SPA server, Http?s request proxy, API overview page, File explorer",
|
|
5
5
|
"main": "./dist/mockServer.js",
|
|
6
6
|
"bin": {
|
|
@@ -81,7 +81,10 @@
|
|
|
81
81
|
"tap": "^15.0.10"
|
|
82
82
|
},
|
|
83
83
|
"tap": {
|
|
84
|
-
"check-coverage": false
|
|
84
|
+
"check-coverage": false,
|
|
85
|
+
"nyc-arg": [
|
|
86
|
+
"--include=src/**/*.js"
|
|
87
|
+
]
|
|
85
88
|
},
|
|
86
89
|
"husky": {
|
|
87
90
|
"hooks": {
|