151 lines
7.5 KiB
JavaScript
151 lines
7.5 KiB
JavaScript
"use strict";
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const iconsDir = path.resolve(__dirname, "svg");
|
|
const outFile = path.resolve(__dirname, "gallery.html");
|
|
|
|
if (!fs.existsSync(iconsDir)) {
|
|
console.error(`SVG directory not found: ${iconsDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const entries = fs.readdirSync(iconsDir, { withFileTypes: true })
|
|
.filter(d => d.isFile() && d.name.toLowerCase().endsWith(".svg"))
|
|
.map(d => ({ name: d.name, size: fs.statSync(path.join(iconsDir, d.name)).size }))
|
|
.sort((a, b) => a.name.localeCompare(b.name, "zh", { sensitivity: "base", numeric: true }));
|
|
|
|
const empty = entries.filter(e => e.size === 0);
|
|
const nonEmpty = entries.filter(e => e.size > 0);
|
|
|
|
const escapeHtml = (s) => String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/\"/g, """)
|
|
.replace(/'/g, "'");
|
|
|
|
function renderCard(file, kind) {
|
|
const display = file.name.replace(/\.svg$/i, "");
|
|
const safe = escapeHtml(display);
|
|
const dataName = display.toLowerCase();
|
|
const src = "svg/" + encodeURIComponent(file.name);
|
|
if (kind === 'empty') {
|
|
return `\n <div class="card empty" data-name="${escapeHtml(dataName)}">\n <div class="preview placeholder"></div>\n <div class="name" title="${safe}">${safe}</div>\n </div>`;
|
|
}
|
|
return `\n <div class="card" data-name="${escapeHtml(dataName)}">\n <div class="preview"><img loading="lazy" src="${src}" alt="${safe}" onerror="this.closest('div.card').classList.add('error')"></div>\n <div class="name" title="${safe}">${safe}</div>\n </div>`;
|
|
}
|
|
|
|
const rowsNonEmpty = nonEmpty.map(f => renderCard(f, 'icon')).join("");
|
|
const rowsEmpty = empty.map(f => renderCard(f, 'empty')).join("");
|
|
|
|
const html = `<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>SVG 图标预览</title>
|
|
<style>
|
|
:root { --gap: 12px; --size: 64px; --border: #e5e7eb; --text: #111827; --muted: #6b7280; --error: #ef4444; --checker-a:#e5e7eb; --checker-b:#ffffff; }
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; color: var(--text); }
|
|
header { position: sticky; top: 0; background: #fff; border-bottom: 1px solid var(--border); padding: 14px 16px; display: flex; align-items: center; gap: 12px; z-index: 10; }
|
|
h1 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
.search { flex: 1; }
|
|
.search input { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; }
|
|
.count { font-size: 12px; color: var(--muted); white-space: nowrap; }
|
|
main { padding: 16px; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--gap); }
|
|
.card { border: 1px solid var(--border); border-radius: 12px; padding: 12px; display: flex; flex-direction: column; align-items: center; gap: 10px; background: #fff; }
|
|
.preview { width: var(--size); height: var(--size); border-radius: 10px; display: flex; align-items: center; justify-content: center; background: repeating-conic-gradient(var(--checker-a) 0 25%, var(--checker-b) 0 50%) 50% / 12px 12px; }
|
|
.preview img { max-width: 100%; max-height: 100%; filter: drop-shadow(0 0 1px rgba(0,0,0,.45)) drop-shadow(0 0 2px rgba(0,0,0,.25)); }
|
|
.name { text-align: center; font-size: 12px; color: var(--muted); width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.card.error { border-color: var(--error); }
|
|
.card.error .name { color: var(--error); }
|
|
.card.empty { border-color: #f59e0b; }
|
|
.card.empty .preview { background: repeating-conic-gradient(#fef3c7 0 25%, #fff7ed 0 50%) 50% / 12px 12px; }
|
|
footer { padding: 16px; color: var(--muted); text-align: center; font-size: 12px; }
|
|
.toolbar { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 12px; }
|
|
.size-input { width: 120px; }
|
|
select, button, label.switch { border: 1px solid var(--border); background: #fff; padding: 6px 8px; border-radius: 6px; }
|
|
label.switch { display: inline-flex; align-items: center; gap: 6px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>SVG 图标预览</h1>
|
|
<div class="toolbar">
|
|
<label>大小: <input class="size-input" type="range" min="24" max="160" value="64"></label>
|
|
<label>背景:
|
|
<select id="bg">
|
|
<option value="checker" selected>棋盘</option>
|
|
<option value="white">白色</option>
|
|
<option value="gray">浅灰</option>
|
|
<option value="dark">深色</option>
|
|
</select>
|
|
</label>
|
|
<label class="switch"><input id="toggleEmpty" type="checkbox"> 显示空文件(${empty.length})</label>
|
|
</div>
|
|
<div class="search"><input id="q" type="search" placeholder="搜索图标名称...(支持中英文)" autofocus></div>
|
|
<div class="count"><span id="shown"></span>/<span id="total"></span></div>
|
|
</header>
|
|
<main>
|
|
<div id="grid" class="grid">${rowsNonEmpty}${rowsEmpty}\n </div>
|
|
</main>
|
|
<footer>共 ${nonEmpty.length} 个有效图标,${empty.length} 个空文件。默认隐藏空文件,你可在工具栏勾选显示。</footer>
|
|
<script>
|
|
const q = document.getElementById('q');
|
|
const grid = document.getElementById('grid');
|
|
const cards = Array.from(grid.querySelectorAll('.card'));
|
|
const total = document.getElementById('total');
|
|
const shown = document.getElementById('shown');
|
|
const bg = document.getElementById('bg');
|
|
const toggleEmpty = document.getElementById('toggleEmpty');
|
|
total.textContent = String(cards.length);
|
|
|
|
function applyFilter() {
|
|
const term = q.value.trim().toLowerCase();
|
|
let visible = 0;
|
|
for (const el of cards) {
|
|
const isEmpty = el.classList.contains('empty');
|
|
const matches = !term || el.dataset.name.includes(term);
|
|
const show = matches && (toggleEmpty.checked || !isEmpty);
|
|
el.style.display = show ? '' : 'none';
|
|
if (show) visible++;
|
|
}
|
|
shown.textContent = String(visible);
|
|
}
|
|
q.addEventListener('input', applyFilter);
|
|
toggleEmpty.addEventListener('change', applyFilter);
|
|
applyFilter();
|
|
|
|
const sizeInput = document.querySelector('.size-input');
|
|
function applySize() { document.documentElement.style.setProperty('--size', sizeInput.value + 'px'); }
|
|
sizeInput.addEventListener('input', applySize);
|
|
applySize();
|
|
|
|
function applyBg() {
|
|
const v = bg.value;
|
|
if (v === 'checker') {
|
|
document.documentElement.style.setProperty('--checker-a', '#e5e7eb');
|
|
document.documentElement.style.setProperty('--checker-b', '#ffffff');
|
|
} else if (v === 'white') {
|
|
document.documentElement.style.setProperty('--checker-a', '#ffffff');
|
|
document.documentElement.style.setProperty('--checker-b', '#ffffff');
|
|
} else if (v === 'gray') {
|
|
document.documentElement.style.setProperty('--checker-a', '#e5e7eb');
|
|
document.documentElement.style.setProperty('--checker-b', '#e5e7eb');
|
|
} else if (v === 'dark') {
|
|
document.documentElement.style.setProperty('--checker-a', '#111827');
|
|
document.documentElement.style.setProperty('--checker-b', '#1f2937');
|
|
}
|
|
}
|
|
bg.addEventListener('change', applyBg);
|
|
applyBg();
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
|
|
const utf8NoBom = new (require('util').TextEncoder)();
|
|
fs.writeFileSync(outFile, html, { encoding: "utf8" });
|
|
console.log(`Generated: ${outFile} with ${nonEmpty.length} icons and ${empty.length} empty files`); |