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 CHANGED
@@ -1,7 +1,6 @@
1
1
  [![release status](https://github.com/chandq/mock-service-cli/actions/workflows/release.yml/badge.svg)](https://github.com/chandq/mock-service-cli/actions/workflows/release.yml)
2
2
  [![Coverage Status](https://coveralls.io/repos/github/chandq/mock-service-cli/badge.svg?branch=master)](https://coveralls.io/github/chandq/mock-service-cli?branch=master)
3
3
  [![mock-service-cli](https://img.shields.io/github/package-json/v/chandq/mock-service-cli?style=flat-square)](https://www.npmjs.com/package/mock-service-cli)
4
- [![node](https://img.shields.io/badge/language-node-orange.svg)](https://nodejs.org/download/release/v12.0.0/)
5
4
  [![license:MIT](https://img.shields.io/npm/l/vue.svg?sanitize=true)](https://github.com/chandq/mock-service-cli/blob/master/LICENSE.md)
6
5
 
7
6
  # 🦅 mock-service-cli
@@ -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: grid;
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: all 0.2s;
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: 37px;
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: flex;
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 && currentView === 'grid') {
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(1, Math.floor((Math.max(fileGrid.clientWidth, GRID_MIN_ITEM_WIDTH + 16) - 8) / (GRID_MIN_ITEM_WIDTH + 8)));
1619
- const rowHeight = isListView ? LIST_ROW_HEIGHT : GRID_ROW_HEIGHT;
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 = { files, columnCount, rowHeight, isListView, spacer, content, startRow: -1, endRow: -1 };
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 { files, columnCount, rowHeight, isListView, content } = virtualState;
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(Math.ceil(files.length / columnCount), firstVisibleRow + visibleRows + VIRTUAL_BUFFER_ROWS);
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
- const fragment = document.createDocumentFragment();
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
- for (let index = startIndex; index < endIndex; index++) {
1661
- fragment.appendChild(createFileItem(files[index], isListView));
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
- content.style.transform = `translateY(${startRow * rowHeight}px)`;
1665
- content.replaceChildren(fragment);
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 createFileItem(file, isListView) {
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 = `file-item ${hiddenClass} ${selectedClass}`;
1681
- fileItem.dataset.path = file.path;
1682
- fileItem.dataset.isDirectory = file.isDirectory;
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
- fileItem.innerHTML = `
1686
- <input class="file-select edit-only edit-inline-block" type="checkbox" data-action="toggleSelect" data-path="${pathAttr}" title="${selectLabel}" ${selectedAttr} />
1687
- <div class="file-icon">${icon}</div>
1688
- <div class="file-info"><div class="file-name">${escapeHtml(file.name)}</div><div class="file-meta">${file.isDirectory ? '<span>目录</span>' : `<span>${size}</span>`}<span>${mtime}</span><span class="birthtime" style="display: ${showBirthtime ? 'inline-block' : 'none'};">${birthtime}</span></div></div>
1689
- <div class="action-buttons">
1690
- <button class="copy-btn" data-action="copyPath" data-path="${pathAttr}" title="复制路径">📋</button>
1691
- <button class="action-btn open" data-action="openInExplorer" data-path="${pathAttr}" title="在文件管理器中打开">📂</button>
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
- function getFileIcon(file) {
2488
+ const fileIconInfoCache = new Map();
2489
+
2490
+ function getFileIconInfo(file) {
2182
2491
  if (file.isDirectory) {
2183
- return `<svg viewBox="0 0 24 24" fill="none" stroke="#667eea" stroke-width="2">
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 color = iconColors[ext] || '#95a5a6';
2267
-
2268
- return `<svg viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2">
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(dateStr) {
2283
- const date = new Date(dateStr);
2284
- return date.toLocaleString('zh-CN');
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(/[&<>"']/g, char => ({
2295
- '&': '&amp;',
2296
- '<': '&lt;',
2297
- '>': '&gt;',
2298
- '"': '&quot;',
2299
- "'": '&#39;'
2300
- })[char]);
2606
+ return String(value).replace(
2607
+ /[&<>"']/g,
2608
+ char =>
2609
+ ({
2610
+ '&': '&amp;',
2611
+ '<': '&lt;',
2612
+ '>': '&gt;',
2613
+ '"': '&quot;',
2614
+ "'": '&#39;'
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.0",
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": {