Files
aurora-admin/site/icon-preview.html

399 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图标预览 · Kole UI</title>
<meta name="description" content="Kole UI 图标预览:2574 个图标的网格浏览、搜索、按名/按代码复制、尺寸调节。">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="../.design_library/kole-ui/colors_and_type.css">
<style>
/* 图标预览(IconPreview)— 对齐 .design_library/kole-ui/specs/组件5.txt「图标预览 IconPreview」:
展示系统所有图标 / 网格布局,支持搜索 / 点击图标复制名称或代码 / 可调整图标大小
自包含样式:不引 site/style.css(本页是工具页,与文档站 SPA 解耦,同 playground.html 的取向)。 */
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: var(--kole-font-family);
color: var(--kole-color-text-body);
background: var(--kole-color-page-bg);
}
.ip-shell { max-width: 1280px; margin: 0 auto; padding: 24px; }
.ip-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 4px; }
.ip-head h1 { margin: 0; font-size: 22px; color: var(--kole-color-text-title); }
.ip-head .back { font-size: 13px; color: var(--kole-color-brand); text-decoration: none; }
.ip-head .back:hover { text-decoration: underline; }
.ip-sub { margin: 0 0 18px; font-size: 13px; color: var(--kole-color-text-secondary); }
/* ---------- 工具条 ---------- */
.ip-toolbar {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
padding: 12px 16px; margin-bottom: 16px;
background: var(--kole-color-card-bg);
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-radius-large);
}
.ip-field { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--kole-color-text-secondary); }
.ip-input {
height: 32px; min-width: 220px; padding: 0 10px;
font: inherit; font-size: 13px;
color: var(--kole-color-text-body);
background: var(--kole-color-card-bg);
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-radius-base);
}
.ip-input:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: 1px; border-color: var(--kole-color-brand); }
/* size 档位:对齐图标 API 的四档关键字(ICON-SPEC §4.2) */
.ip-sizes { display: inline-flex; border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); overflow: hidden; }
.ip-size {
height: 32px; padding: 0 12px; border: 0; border-inline-end: 1px solid var(--kole-color-border);
background: var(--kole-color-card-bg); color: var(--kole-color-text-body);
font: inherit; font-size: 13px; cursor: pointer;
}
.ip-size:last-child { border-inline-end: 0; }
.ip-size:hover { background: var(--kole-color-table-header-bg); }
.ip-size[aria-pressed="true"] { background: var(--kole-color-brand); color: var(--kole-color-text-inverse); }
.ip-size:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: -2px; }
.ip-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--kole-color-text-body); cursor: pointer; }
.ip-count { margin-inline-start: auto; font-size: 13px; color: var(--kole-color-text-secondary); font-family: var(--kole-font-family-num); }
/* ---------- 分组 ---------- */
.ip-group { margin-bottom: 20px; }
.ip-group > summary {
cursor: pointer; padding: 8px 4px; font-size: 14px; font-weight: 600;
color: var(--kole-color-text-title); list-style: none; display: flex; align-items: center; gap: 8px;
}
.ip-group > summary::-webkit-details-marker { display: none; }
.ip-group > summary::before { content: '▸'; color: var(--kole-color-text-secondary); font-size: 12px; }
.ip-group[open] > summary::before { content: '▾'; }
.ip-group .badge {
font-family: var(--kole-font-family-num); font-size: 12px; font-weight: 400;
padding: 1px 7px; border-radius: var(--kole-radius-small);
background: var(--kole-color-table-header-bg); color: var(--kole-color-text-secondary);
}
/* ---------- 网格 ---------- */
.ip-grid {
display: grid; gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
padding: 4px 0 8px;
}
.ip-cell {
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
min-height: 96px; padding: 12px 8px;
background: var(--kole-color-card-bg);
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-radius-medium);
cursor: pointer; text-align: center;
font: inherit; color: inherit;
}
.ip-cell:hover { border-color: var(--kole-color-brand); background: var(--kole-color-brand-bg); }
.ip-cell:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: 2px; }
.ip-cell .glyph { display: flex; align-items: center; justify-content: center; color: var(--kole-color-text-title); }
/* 尺寸档位改的是 SVG 的 width/height 属性(见页尾 .ip-size 的点击处理)。
这里**不能**写 `width: 1em; height: 1em` —— 作者样式表压过表现属性,
四档会全部渲染成 .glyph 的 font-size(实测恒为 16px,2026-09-23 修复,
详见 ROADMAP S9-P4)。display:block 保留,去掉行内基线的多余间隙。 */
.ip-cell .glyph svg { display: block; }
.ip-cell .label {
font-size: 11px; line-height: 1.35; color: var(--kole-color-text-secondary);
word-break: break-all; font-family: var(--kole-font-family-num);
}
.ip-cell.is-copied { border-color: var(--kole-color-success); background: color-mix(in srgb, var(--kole-color-success) 8%, var(--kole-color-card-bg)); }
.ip-cell.is-copied .label { color: var(--kole-color-success); }
.ip-empty { padding: 40px 16px; text-align: center; color: var(--kole-color-text-secondary); font-size: 14px; }
/* 提示条:复制反馈(同 playground 的做法,不引第三方 toast) */
.ip-toast {
position: fixed; inset-inline: 0; bottom: 24px; margin: 0 auto; width: fit-content; max-width: 90vw;
padding: 9px 16px; border-radius: var(--kole-radius-base);
background: var(--kole-color-text-title); color: var(--kole-color-text-inverse);
font-size: 13px; opacity: 0; transition: opacity .18s; pointer-events: none; z-index: 50;
}
.ip-toast.show { opacity: 1; }
.ip-note {
margin: 20px 0 0; padding: 12px 16px; font-size: 12px; line-height: 1.7;
color: var(--kole-color-text-secondary);
background: var(--kole-color-table-header-bg);
border-radius: var(--kole-radius-base);
}
.ip-note code { font-family: var(--kole-font-family-num); }
@media (prefers-reduced-motion: reduce) { .ip-toast { transition: none; } }
</style>
</head>
<body>
<div class="ip-shell">
<div class="ip-head">
<h1>图标预览</h1>
<a class="back" href="./">← 返回文档站</a>
</div>
<p class="ip-sub">2574 个图标 · 点击任意图标复制名称(Shift + 点击复制内联 SVG 代码)</p>
<div class="ip-toolbar">
<label class="ip-field">
搜索
<input id="ip-q" class="ip-input" type="search" placeholder="图标名,如 search / 箭头 / chart" autocomplete="off">
</label>
<div class="ip-sizes" role="group" aria-label="图标大小">
<button class="ip-size" type="button" data-size="16" aria-pressed="false">16</button>
<button class="ip-size" type="button" data-size="20" aria-pressed="true">20</button>
<button class="ip-size" type="button" data-size="24" aria-pressed="false">24</button>
<button class="ip-size" type="button" data-size="32" aria-pressed="false">32</button>
</div>
<label class="ip-toggle"><input id="ip-stroke-only" type="checkbox"> 仅线性</label>
<span id="ip-count" class="ip-count">—</span>
</div>
<div id="ip-groups"></div>
<p id="ip-empty" class="ip-empty" hidden>没有匹配的图标。试试更短的关键词,或清空搜索。</p>
<p class="ip-note">
图标名直接用在组件上:<code>&lt;KoleIcon name="search" /&gt;</code>(PC)与
<code>&lt;Icon name="search" /&gt;</code>(移动端)。<br>
数据真源 <code>.design_library/kole-ui/icons/</code>;来源与许可见
<code>ATTRIBUTION.md</code>(TDesign + Element Plus,MIT)。描边宽度统一 1.5px。
</p>
</div>
<div id="ip-toast" class="ip-toast" role="status" aria-live="polite"></div>
<script>
/* 图标预览页 —— 自包含实现(零依赖、无外部脚本),同 playground.html 的取向。
*
* 数据分两段加载:
* 1) icons.json(51KB)—— 名字 / 层 / 分组,首屏立即出网格骨架;
* 2) registry.json(670KB)—— 路径数据,按需装载,装载前网格显示占位。
* 为什么不合并:合并后首屏要等 670KB,而用户往往只搜几个图标。
*/
(function () {
var SIZE = 20;
var DATA = null; /* registry:{ icons: { name: 'S…|F…' } } */
var INDEX = null; /* icons.json:{ names:[], tier:[], group:[], groups:[] } */
var GROUPS = []; /* 分组名,来自 icons.json */
function $(id) { return document.getElementById(id); }
/* ---------- 渲染 ---------- */
function viewBoxOf(name, raw) {
var vb = (DATA && DATA.viewBox) || {};
/* 例外表按 canonical 名给;别名与 canonical 路径串相同,按路径反查覆盖别名 */
if (vb[name]) return vb[name];
if (raw && DATA && DATA.byPath && DATA.byPath[raw]) return DATA.byPath[raw];
return '0 0 24 24';
}
function svgFor(name, sizePx) {
var raw = DATA.icons[name];
if (!raw) return null;
var sw = (DATA && DATA.strokeWidth) || 1.5;
var segs = raw.split('|');
var body = '';
for (var i = 0; i < segs.length; i++) {
var seg = segs[i];
var mode = seg.charAt(0) === 'S' ? 'stroke' : 'fill';
var d = seg.slice(1);
body += mode === 'stroke'
? '<path d="' + d + '" fill="none" stroke="currentColor" stroke-width="' + sw + '" stroke-linecap="round" stroke-linejoin="round"/>'
: '<path d="' + d + '" fill="currentColor"/>';
}
return '<svg viewBox="' + viewBoxOf(name, raw) + '" width="' + sizePx + '" height="' + sizePx +
'" aria-hidden="true" focusable="false">' + body + '</svg>';
}
function buildByPath() {
/* 路径串 → viewBox 反查表(覆盖别名) */
var vb = (DATA && DATA.viewBox) || {};
var icons = DATA.icons;
var out = {};
for (var k in vb) {
if (icons[k] && !out[icons[k]]) out[icons[k]] = vb[k];
}
DATA.byPath = out;
}
function matches(name, q) {
if (!q) return true;
return name.toLowerCase().indexOf(q) >= 0;
}
/* "仅线性"判据:该图标没有任何填充段 */
function isStrokeOnly(name) {
var raw = DATA && DATA.icons[name];
if (!raw) return true;
return raw.split('|').every(function (s) { return s.charAt(0) === 'S'; });
}
function render() {
var q = ($('ip-q').value || '').trim().toLowerCase();
var strokeOnly = $('ip-stroke-only').checked;
var host = $('ip-groups');
host.textContent = '';
if (!DATA) {
/* 路径数据还没到:先用索引渲染名字(图标位留空),避免白屏 */
var pending = indexNames().filter(function (n) { return matches(n, q); });
$('ip-count').textContent = pending.length + ' / ' + indexNames().length + '(加载图标数据…)';
return;
}
var total = 0;
var shown = 0;
GROUPS.forEach(function (g, gi) {
var names = [];
for (var i = 0; i < INDEX.names.length; i++) {
if (INDEX.group[i] !== gi) continue;
total++;
var n = INDEX.names[i];
if (!matches(n, q)) continue;
if (strokeOnly && !isStrokeOnly(n)) continue;
names.push(n);
}
if (!names.length) return;
shown += names.length;
var d = document.createElement('details');
/* 搜索时展开全部(否则用户搜到了却看不见);无搜索词时只展开前两组 */
d.className = 'ip-group';
d.open = !!q || gi < 2;
var sum = document.createElement('summary');
sum.innerHTML = g + ' <span class="badge">' + names.length + '</span>';
d.appendChild(sum);
var grid = document.createElement('div');
grid.className = 'ip-grid';
names.forEach(function (name) {
var cell = document.createElement('button');
cell.type = 'button';
cell.className = 'ip-cell';
cell.setAttribute('data-name', name);
cell.setAttribute('aria-label', '图标 ' + name + ',点击复制名称');
var glyph = document.createElement('span');
glyph.className = 'glyph';
glyph.innerHTML = svgFor(name, SIZE) || '';
var lab = document.createElement('span');
lab.className = 'label';
lab.textContent = name;
cell.appendChild(glyph);
cell.appendChild(lab);
grid.appendChild(cell);
});
d.appendChild(grid);
host.appendChild(d);
});
$('ip-count').textContent = shown + ' / ' + total;
$('ip-empty').hidden = shown > 0;
}
function indexNames() { return (INDEX && INDEX.names) || []; }
/* ---------- 复制 ---------- */
var toastTimer = null;
function toast(msg) {
var t = $('ip-toast');
t.textContent = msg;
t.classList.add('show');
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () { t.classList.remove('show'); }, 1600);
}
function copyText(text) {
/* navigator.clipboard 需要安全上下文(https / localhost);http 局域网下回退 execCommand */
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
return new Promise(function (resolve, reject) {
var ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed')); }
catch (e) { reject(e); }
finally { document.body.removeChild(ta); }
});
}
function flash(cell) {
cell.classList.add('is-copied');
setTimeout(function () { cell.classList.remove('is-copied'); }, 700);
}
/* ---------- 事件 ---------- */
$('ip-groups').addEventListener('click', function (e) {
var cell = e.target.closest('.ip-cell');
if (!cell) return;
var name = cell.getAttribute('data-name');
/* Shift + 点击复制内联 SVG 代码(规范要求「复制名称或代码」) */
var payload = e.shiftKey ? (svgFor(name, SIZE) || name) : name;
copyText(payload).then(function () {
flash(cell);
toast(e.shiftKey ? '已复制 SVG:' + name : '已复制名称:' + name);
}).catch(function () {
toast('复制失败,请手动选中:' + name);
});
});
$('ip-q').addEventListener('input', render);
$('ip-stroke-only').addEventListener('change', render);
Array.prototype.forEach.call(document.querySelectorAll('.ip-size'), function (btn) {
btn.addEventListener('click', function () {
SIZE = Number(btn.getAttribute('data-size'));
Array.prototype.forEach.call(document.querySelectorAll('.ip-size'), function (b) {
b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');
});
Array.prototype.forEach.call(document.querySelectorAll('.ip-cell'), function (c) {
var g = c.querySelector('.glyph');
if (!g) return;
var svg = g.querySelector('svg');
if (svg) { svg.setAttribute('width', SIZE); svg.setAttribute('height', SIZE); }
});
});
});
/* ---------- 装载 ---------- */
function loadIndex() {
return fetch('icons.json').then(function (r) { return r.ok ? r.json() : null; });
}
function loadRegistry() {
return fetch('../.design_library/kole-ui/icons/registry.json').then(function (r) {
return r.ok ? r.json() : null;
});
}
loadIndex().then(function (idx) {
if (!idx) { $('ip-count').textContent = '索引加载失败'; return; }
INDEX = idx;
GROUPS = idx.groups || [];
render();
return loadRegistry().then(function (reg) {
if (!reg) { $('ip-count').textContent = '图标数据加载失败'; return; }
/* registry.json 的键是 icons(与 build-icons.mjs 产物一致) */
DATA = { icons: reg.icons || {}, viewBox: reg.viewBox || {}, strokeWidth: reg.strokeWidth || 1.5 };
if (!Object.keys(DATA.viewBox).length) {
/* viewBox 例外表在 registry 里可能只含非默认项;缺失时按 24 网格渲染 */
}
buildByPath();
render();
});
}).catch(function () {
$('ip-count').textContent = '加载失败';
});
}());
</script>
</body>
</html>