329 lines
15 KiB
HTML
329 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<link rel="icon" type="image/svg+xml" href="logo/favicon.svg">
|
||
<title>素材中心 · 画个标</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
|
||
font-size: 14px; color: #1f2937; background: #f3f4f6; min-height: 100vh;
|
||
}
|
||
a { color: #4f46e5; text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
.hidden { display: none !important; }
|
||
|
||
/* ===== 顶栏 ===== */
|
||
.topbar {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 20px; height: 54px;
|
||
background: linear-gradient(90deg, #1e1b4b, #312e81);
|
||
color: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.15);
|
||
position: sticky; top: 0; z-index: 20;
|
||
}
|
||
.topbar .brand { display: flex; align-items: center; gap: 10px; }
|
||
.topbar .brand .icon { font-size: 20px; }
|
||
.topbar .brand .name { font-size: 17px; font-weight: 700; letter-spacing: 1px; }
|
||
.topbar .brand .tag {
|
||
font-size: 11px; color: #c7d2fe; background: rgba(255,255,255,.12);
|
||
padding: 2px 8px; border-radius: 20px;
|
||
}
|
||
.btn-back {
|
||
border: 1px solid rgba(255,255,255,.35); border-radius: 6px;
|
||
background: rgba(255,255,255,.1); color: #fff; padding: 6px 14px;
|
||
font-size: 13px; cursor: pointer; transition: background .15s;
|
||
}
|
||
.btn-back:hover { background: rgba(255,255,255,.22); }
|
||
|
||
.wrap { max-width: 1080px; margin: 22px auto 60px; padding: 0 16px; }
|
||
|
||
/* ===== 概览统计 ===== */
|
||
.stats {
|
||
display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 18px;
|
||
}
|
||
.stat-card {
|
||
flex: 1; min-width: 150px; background: #fff; border: 1px solid #e5e7eb;
|
||
border-radius: 10px; padding: 14px 18px; box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
||
}
|
||
.stat-card .num { font-size: 26px; font-weight: 800; color: #4f46e5; }
|
||
.stat-card .lab { font-size: 12px; color: #6b7280; margin-top: 2px; }
|
||
|
||
/* ===== 指引 ===== */
|
||
.guide {
|
||
background: #eef2ff; border: 1px solid #c7d2fe; border-radius: 10px;
|
||
padding: 14px 18px; margin-bottom: 20px; font-size: 13px; line-height: 1.8;
|
||
}
|
||
.guide b { color: #4338ca; }
|
||
.guide code {
|
||
background: #e0e7ff; border-radius: 4px; padding: 1px 6px;
|
||
font-size: 12px; color: #3730a3;
|
||
}
|
||
.guide ol { margin: 6px 0 0 20px; }
|
||
|
||
/* ===== 工具栏(分类 Tab + 搜索) ===== */
|
||
.toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
||
.filters { display: flex; gap: 6px; flex-wrap: wrap; }
|
||
.fchip {
|
||
border: 1px solid #e5e7eb; background: #fff; color: #4b5563;
|
||
padding: 6px 14px; border-radius: 20px; font-size: 13px; cursor: pointer;
|
||
transition: all .15s;
|
||
}
|
||
.fchip:hover { border-color: #c7d2fe; color: #4338ca; }
|
||
.fchip.active { background: #4f46e5; border-color: #4f46e5; color: #fff; }
|
||
.search {
|
||
flex: 1; min-width: 200px; border: 1px solid #e5e7eb; border-radius: 8px;
|
||
padding: 8px 12px; font-size: 13px; color: #1f2937; outline: none;
|
||
}
|
||
.search:focus { border-color: #4f46e5; }
|
||
|
||
/* ===== 来源卡片 ===== */
|
||
.grid {
|
||
display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
.card {
|
||
background: #fff; border: 1px solid #e5e7eb; border-radius: 12px;
|
||
padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
||
display: flex; flex-direction: column; gap: 10px;
|
||
transition: box-shadow .15s, transform .15s;
|
||
}
|
||
.card:hover { box-shadow: 0 4px 14px rgba(79,70,229,.12); transform: translateY(-1px); }
|
||
.card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.card-title { font-size: 15px; font-weight: 700; }
|
||
.card-en { font-size: 11px; color: #6b7280; }
|
||
.tag {
|
||
font-size: 11px; padding: 2px 8px; border-radius: 20px; white-space: nowrap;
|
||
}
|
||
.tag.line { background: #dbeafe; color: #1d4ed8; }
|
||
.tag.fill { background: #fee2e2; color: #b91c1c; }
|
||
.tag.mix { background: #fef3c7; color: #b45309; }
|
||
.tag.brand{ background: #e0e7ff; color: #4338ca; }
|
||
.card-desc { font-size: 12px; color: #4b5563; line-height: 1.6; min-height: 34px; }
|
||
.samples {
|
||
display: flex; gap: 6px; background: #f9fafb; border: 1px solid #f3f4f6;
|
||
border-radius: 8px; padding: 8px 10px; min-height: 44px; align-items: center;
|
||
}
|
||
.samples svg { width: 24px; height: 24px; flex-shrink: 0; }
|
||
.card-meta { display: grid; grid-template-columns: 64px 1fr; gap: 4px 8px; font-size: 12px; }
|
||
.card-meta dt { color: #6b7280; }
|
||
.card-meta dd { color: #1f2937; word-break: break-all; }
|
||
.license-cc { color: #b45309; font-weight: 600; }
|
||
.card-foot {
|
||
margin-top: auto; padding-top: 8px; border-top: 1px dashed #e5e7eb;
|
||
display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: #9ca3af;
|
||
}
|
||
.foot-links { display: flex; gap: 10px; }
|
||
.foot-links a { color: #4f46e5; }
|
||
|
||
.empty { text-align: center; color: #6b7280; padding: 60px 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<div class="brand">
|
||
<img class="icon" src="logo/logo-mark.svg" alt="画个标" width="20" height="20">
|
||
<span class="name">画个标</span>
|
||
<span class="tag">素材中心</span>
|
||
</div>
|
||
<button class="btn-back" onclick="location.href='materials.html'">🗃 素材库</button>
|
||
<button class="btn-back" onclick="location.href='index.html'">← 返回设计工作台</button>
|
||
</div>
|
||
|
||
<div class="wrap">
|
||
<div class="stats">
|
||
<div class="stat-card"><div class="num" id="st-sources">-</div><div class="lab">素材来源(个)</div></div>
|
||
<div class="stat-card"><div class="num" id="st-icons">-</div><div class="lab">内置图标总数(个)</div></div>
|
||
<div class="stat-card"><div class="num" id="st-cats">-</div><div class="lab">素材分类(类)</div></div>
|
||
<div class="stat-card"><div class="num" id="st-time">-</div><div class="lab">最后构建时间</div></div>
|
||
</div>
|
||
|
||
<div class="guide">
|
||
<b>📦 素材中心说明</b>
|
||
<div>所有图标 / 表情 / 品牌 Logo 素材均已<b>离线内置</b>(构建时一次性下载),页面运行无需联网;按<b>单色图标 · 多色表情 · 品牌标识</b>三大类分类管理,可按关键词检索<b>中文名 / 英文名 / 描述</b>,来源、协议与构建信息清晰可查。</div>
|
||
<div style="margin-top:6px; color:#3730a3;">协议以各来源官方为准,商用前请核对(CC BY 4.0 需署名)。如何补充新源见下方原指引。</div>
|
||
</div>
|
||
|
||
<div class="toolbar">
|
||
<div class="filters" id="filters"></div>
|
||
<input class="search" id="search" placeholder="🔍 搜索素材(中文名 / 英文名 / 描述)">
|
||
</div>
|
||
|
||
<div class="grid" id="grid"></div>
|
||
<div class="empty hidden" id="empty">加载图标来源数据失败,请确认 js/data/icon-sources.js 已加载。</div>
|
||
</div>
|
||
|
||
<script src="js/data/icon-sources.js?v=20260823g"></script>
|
||
<script src="js/data/builtin-icons.js?v=20260823g"></script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
// 优先读机器可读 manifest(js/data/manifest/icons.json,构建时生成);失败回退内置配置
|
||
var SOURCES = [];
|
||
var BUILD_AT = null;
|
||
var FALLBACK = window.HGB_IconSources || [];
|
||
// builtin-icons.js 用 const 声明(全局词法绑定,非 window 属性),用 typeof 探测
|
||
var BUILTIN = (typeof HGB_BuiltinIcons !== 'undefined') ? HGB_BuiltinIcons : null;
|
||
|
||
function resolveSources(cb) {
|
||
fetch('js/data/manifest/icons.json?v=20260823g')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (m) {
|
||
SOURCES = m.sources || [];
|
||
BUILD_AT = m.builtAt || null;
|
||
cb();
|
||
})
|
||
.catch(function () { SOURCES = FALLBACK; cb(); });
|
||
}
|
||
|
||
/* 描边型判断(与 panels.js isStrokeIcon 一致) */
|
||
var STROKE_SOURCES = new Set(['feather', 'lucide', 'tabler', 'iconoir']);
|
||
function isStroke(ic, sid) {
|
||
return STROKE_SOURCES.has(sid) ||
|
||
/stroke\s*=|\bstroke-linecap\s*=|\bstroke-linejoin\s*=/.test(ic && ic.svg || '');
|
||
}
|
||
function iconAttrs(ic, sid) {
|
||
// 多色素材(emoji/品牌)保留原色,不强制单色
|
||
if (ic.type && ic.type !== 'icon') return 'fill="inherit" stroke="inherit"';
|
||
return isStroke(ic, sid)
|
||
? 'fill="none" stroke="#4f46e5" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"'
|
||
: 'fill="#4f46e5" stroke="none"';
|
||
}
|
||
function bodyOf(ic) {
|
||
return ic.svg
|
||
.replace(/fill="currentColor"/g, 'fill="inherit"')
|
||
.replace(/stroke="currentColor"/g, 'stroke="inherit"');
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s).replace(/&/g, '&').replace(/</g, '<')
|
||
.replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
var STYLE_TAG = { '线条': 'line', '填充': 'fill', '混合': 'mix', '品牌': 'brand', '多色': 'mix' };
|
||
var TYPE_CAT = { icon: '单色图标', emoji: '多色表情', brand: '品牌标识' };
|
||
var TYPE_EMOJI = { icon: '🔹', emoji: '🌈', brand: '🏷️' };
|
||
var filter = 'all';
|
||
var keyword = '';
|
||
|
||
function renderStats() {
|
||
document.getElementById('st-sources').textContent = SOURCES.length;
|
||
document.getElementById('st-cats').textContent = Object.keys(TYPE_CAT).length;
|
||
var total = 0;
|
||
SOURCES.forEach(function (s) {
|
||
var count = (typeof s.count === 'number') ? s.count
|
||
: (BUILTIN && BUILTIN.sources && BUILTIN.sources[s.id]) ? BUILTIN.sources[s.id].count : 0;
|
||
total += (typeof count === 'number' ? count : 0);
|
||
});
|
||
document.getElementById('st-icons').textContent = total.toLocaleString();
|
||
var meta = BUILTIN && BUILTIN.meta;
|
||
var ts = BUILD_AT || (meta && meta.generatedAt);
|
||
if (ts) {
|
||
var d = new Date(ts);
|
||
document.getElementById('st-time').textContent =
|
||
d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||
String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':' +
|
||
String(d.getMinutes()).padStart(2, '0');
|
||
} else {
|
||
document.getElementById('st-time').textContent = '—';
|
||
}
|
||
}
|
||
|
||
function renderFilters() {
|
||
var countByType = {};
|
||
SOURCES.forEach(function (s) { countByType[s.type] = (countByType[s.type] || 0) + 1; });
|
||
var cats = [{ id: 'all', name: '全部 ' + SOURCES.length }]
|
||
.concat(Object.keys(TYPE_CAT).map(function (t) {
|
||
return { id: t, name: (TYPE_EMOJI[t] || '') + ' ' + TYPE_CAT[t] + ' ' + (countByType[t] || 0) };
|
||
}));
|
||
document.getElementById('filters').innerHTML = cats.map(function (c) {
|
||
return '<button class="fchip ' + (filter === c.id ? 'active' : '') + '" data-f="' + esc(c.id) + '">' + esc(c.name) + '</button>';
|
||
}).join('');
|
||
document.querySelectorAll('#filters [data-f]').forEach(function (b) {
|
||
b.addEventListener('click', function () { filter = b.dataset.f; renderFilters(); render(); });
|
||
});
|
||
}
|
||
|
||
function render() {
|
||
if (!SOURCES.length) {
|
||
document.getElementById('empty').classList.remove('hidden');
|
||
return;
|
||
}
|
||
var kw = keyword.trim().toLowerCase();
|
||
var list = SOURCES.filter(function (s) {
|
||
if (filter !== 'all' && s.type !== filter) return false;
|
||
if (!kw) return true;
|
||
return (s.nameCn || '').toLowerCase().includes(kw) ||
|
||
(s.name || '').toLowerCase().includes(kw) ||
|
||
(s.desc || '').toLowerCase().includes(kw) ||
|
||
(s.id || '').toLowerCase().includes(kw);
|
||
});
|
||
var grid = document.getElementById('grid');
|
||
if (!list.length) {
|
||
grid.innerHTML = '';
|
||
document.getElementById('empty').classList.remove('hidden');
|
||
document.getElementById('empty').textContent = '未找到匹配素材,请调整分类或搜索词。';
|
||
return;
|
||
}
|
||
document.getElementById('empty').classList.add('hidden');
|
||
grid.innerHTML = list.map(function (s) {
|
||
var srcData = BUILTIN && BUILTIN.sources && BUILTIN.sources[s.id];
|
||
var count = (typeof s.count === 'number') ? s.count : (srcData ? srcData.count : '—');
|
||
var tagCls = STYLE_TAG[s.style] || 'line';
|
||
// 取前 4 个图标做示例(懒加载拆分后索引只存 samples body,包装为 {svg,type} 复用渲染逻辑)
|
||
var samples = (srcData && srcData.samples
|
||
? srcData.samples.map(function (b) { return { svg: b, type: srcData.type }; })
|
||
: []);
|
||
var samplesHtml = samples.length
|
||
? samples.map(function (ic) {
|
||
return '<svg viewBox="0 0 24 24" ' + iconAttrs(ic, s.id) + '>' + bodyOf(ic) + '</svg>';
|
||
}).join('')
|
||
: '<span style="font-size:11px;color:#9ca3af">构建后显示示例</span>';
|
||
var npm = s.npm || '—';
|
||
var licenseCls = /CC BY/i.test(s.license) ? 'license-cc' : '';
|
||
return '' +
|
||
'<div class="card">' +
|
||
'<div class="card-head">' +
|
||
'<span class="card-title">' + esc(s.nameCn || s.name) + '</span>' +
|
||
'<span class="card-en">' + esc(s.name) + '</span>' +
|
||
'<span class="tag ' + tagCls + '">' + esc(TYPE_CAT[s.type] || s.style || '') + '</span>' +
|
||
'</div>' +
|
||
'<div class="card-desc">' + esc(s.desc || '') + '</div>' +
|
||
'<div class="samples">' + samplesHtml + '</div>' +
|
||
'<dl class="card-meta">' +
|
||
'<dt>协议</dt><dd><span class="' + licenseCls + '">' + esc(s.license || '—') + '</span> ' + esc(s.licenseNote || '') + '</dd>' +
|
||
'<dt>图标数</dt><dd>' + count + '</dd>' +
|
||
'<dt>坐标系</dt><dd>' + esc(s.coordText || '—') + '</dd>' +
|
||
'<dt>变体</dt><dd>' + esc(s.variants || '—') + '</dd>' +
|
||
'<dt>npm 包</dt><dd>' + esc(npm) + '</dd>' +
|
||
'<dt>构建方式</dt><dd>' + esc(s.format || '—') + '</dd>' +
|
||
'</dl>' +
|
||
'<div class="card-foot">' +
|
||
'<span>收录于 ' + esc(s.added || '—') + '</span>' +
|
||
'<span class="foot-links">' +
|
||
(s.home ? '<a href="' + esc(s.home) + '" target="_blank" rel="noopener">官网</a>' : '') +
|
||
(s.github ? '<a href="' + esc(s.github) + '" target="_blank" rel="noopener">GitHub</a>' : '') +
|
||
'</span>' +
|
||
'</div>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
function init() {
|
||
document.getElementById('search').addEventListener('input', function (e) {
|
||
keyword = e.target.value;
|
||
render();
|
||
});
|
||
renderStats();
|
||
renderFilters();
|
||
render();
|
||
}
|
||
|
||
// manifest 异步读取(fetch 失败自动回退内置配置),就绪后再渲染
|
||
resolveSources(init);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|