feat: 初始化 Figma 模板库 56 套展示站

- 56 套模板元数据(35 Figma 原生 + 21 非 Figma)
- 静态展示站 (HTML/CSS/JS 无框架):格式筛选、lightbox、搜索
- 35 个 .fig 已真上传 Figma Drafts 云端
- iframe 实时投射(登录态可看私有 Drafts)
- 部署:nginx:alpine + basic-auth (kang)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kang
2026-04-22 15:31:45 +08:00
commit ee719d07cc
289 changed files with 4016 additions and 0 deletions

173
web/app.js Normal file
View File

@@ -0,0 +1,173 @@
let DATA = null;
let currentFilter = 'all';
let currentSearch = '';
async function load() {
const res = await fetch('data.json?_=' + Date.now());
DATA = await res.json();
renderStats();
render();
document.getElementById('gen').textContent = '生成于 ' + DATA.generated_at;
}
function renderStats() {
const t = DATA.templates;
const fig = t.filter(x => x.has_fig).length;
const sk = t.filter(x => x.has_sketch).length;
const xd = t.filter(x => x.has_xd).length;
const psd = t.filter(x => x.has_psd).length;
const imp = t.filter(x => x.figma_key).length;
const el = document.getElementById('stats');
el.innerHTML = `
<div class="stat"><b>${t.length}</b>套总量</div>
<div class="stat"><b>${fig}</b>Figma 原生</div>
<div class="stat"><b>${sk}</b>含 Sketch</div>
<div class="stat"><b>${xd}</b>含 XD</div>
<div class="stat"><b>${psd}</b>含 PSD</div>
<div class="stat"><b>${imp}</b>已入 Figma Drafts</div>
`;
const banner = document.getElementById('banner');
if (DATA.imported_summary) {
banner.innerHTML = DATA.imported_summary;
banner.classList.add('show');
}
}
function matchFilter(t) {
if (currentFilter === 'all') return true;
if (currentFilter === 'fig') return t.has_fig;
if (currentFilter === 'sketch') return t.has_sketch;
if (currentFilter === 'xd') return t.has_xd;
if (currentFilter === 'psd') return t.has_psd;
if (currentFilter === 'imported') return !!t.figma_key;
return true;
}
function matchSearch(t) {
if (!currentSearch) return true;
return t.name.toLowerCase().includes(currentSearch) ||
t.id.toLowerCase().includes(currentSearch);
}
function render() {
const grid = document.getElementById('grid');
const empty = document.getElementById('empty');
const list = DATA.templates.filter(t => matchFilter(t) && matchSearch(t));
if (list.length === 0) {
grid.innerHTML = '';
empty.hidden = false;
return;
}
empty.hidden = true;
grid.innerHTML = list.map(t => `
<article class="card" data-id="${t.id}">
<div class="cover">
${t.cover ? `<img src="${t.cover}" alt="${escapeHtml(t.name)}" loading="lazy">` : ''}
<span class="id">${t.id}</span>
${t.figma_key ? '<span class="imported">已入 Figma</span>' : ''}
</div>
<div class="body">
<h3>${escapeHtml(t.name)}</h3>
<div class="meta">
<div class="badges">
${t.has_fig ? `<span class="badge fig">FIG${t.fig_count>1?'×'+t.fig_count:''}</span>` : ''}
${t.has_sketch ? `<span class="badge sketch">SKETCH${t.sketch_count>1?'×'+t.sketch_count:''}</span>` : ''}
${t.has_xd ? `<span class="badge xd">XD${t.xd_count>1?'×'+t.xd_count:''}</span>` : ''}
${t.has_psd ? `<span class="badge psd">PSD</span>` : ''}
</div>
<span>${t.archive_size_mb} MB</span>
</div>
</div>
</article>
`).join('');
grid.querySelectorAll('.card').forEach(c => {
c.addEventListener('click', () => openModal(c.dataset.id));
});
}
function openModal(id) {
const t = DATA.templates.find(x => x.id === id);
if (!t) return;
const body = document.getElementById('modalBody');
const figmaSection = t.figma_key
? `<iframe class="figma-embed" src="https://embed.figma.com/design/${t.figma_key}/?embed-host=kang&footer=false" allowfullscreen></iframe>`
: '';
body.innerHTML = `
<h2>${escapeHtml(t.name)}</h2>
<div class="sub2">
<code>${t.id}</code>
<span>${t.archive_size_mb} MB</span>
${t.has_fig ? `<span class="badge fig">FIG×${t.fig_count}</span>` : ''}
${t.has_sketch ? `<span class="badge sketch">SKETCH×${t.sketch_count}</span>` : ''}
${t.has_xd ? `<span class="badge xd">XD×${t.xd_count}</span>` : ''}
${t.has_psd ? `<span class="badge psd">PSD</span>` : ''}
</div>
<div class="actions">
${t.figma_url
? `<a class="btn" href="${t.figma_url}" target="_blank">在 Figma 打开 →</a>`
: (t.has_fig
? `<a class="btn" href="https://www.figma.com/files/recent" target="_blank">在 Figma Drafts 搜 "${escapeAttr(t.name.split(/[\s\-–—]/)[0])}" →</a>`
: '')}
<a class="btn ghost" href="${t.source_rel}" onclick="revealSource('${t.id}');return false;">在 Finder 中显示源包</a>
<a class="btn ghost" href="javascript:void(0)" onclick="copyPath('${t.id}');">复制源文件路径</a>
</div>
${figmaSection}
<div class="spec">
<div class="k">源包</div><div class="v">source/${t.id}/${t.archive}</div>
<div class="k">解压目录</div><div class="v">extracted/${t.id}/</div>
${t.figma_key ? `<div class="k">Figma Key</div><div class="v">${t.figma_key}</div>` : ''}
</div>
<div class="gallery">
${(t.gallery||[]).map(g => `<img src="${g}" loading="lazy">`).join('')}
</div>
`;
document.getElementById('modal').hidden = false;
document.body.style.overflow = 'hidden';
}
function closeModal() {
document.getElementById('modal').hidden = true;
document.body.style.overflow = '';
document.getElementById('modalBody').innerHTML = '';
}
function revealSource(id) {
// Dev-only helper: try opening local path in Finder via file://
// Browsers block file:// from http:// — fall back to a helpful alert
const path = `${location.pathname.replace(/\/[^/]*$/, '')}/../extracted/${id}`;
alert(`本地路径:\n~/Projects/research/20260422-figma模板库/extracted/${id}/\n\n在终端跑:\nopen ~/Projects/research/20260422-figma模板库/extracted/${id}`);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function escapeAttr(s) {
return String(s).replace(/"/g, '&quot;');
}
function copyPath(id) {
const t = DATA.templates.find(x => x.id === id);
if (!t) return;
const p = `~/Projects/research/20260422-figma模板库/extracted/${id}/`;
navigator.clipboard.writeText(p).then(() => {
alert('已复制:\n' + p);
});
}
document.getElementById('filters').addEventListener('click', e => {
const btn = e.target.closest('.pill');
if (!btn) return;
document.querySelectorAll('.pill').forEach(p => p.classList.toggle('active', p === btn));
currentFilter = btn.dataset.filter;
render();
});
document.getElementById('search').addEventListener('input', e => {
currentSearch = e.target.value.trim().toLowerCase();
render();
});
document.getElementById('modalClose').addEventListener('click', closeModal);
document.querySelector('.modal-bg').addEventListener('click', closeModal);
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
load();

1291
web/data.json Normal file

File diff suppressed because it is too large Load Diff

56
web/index.html Normal file
View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Figma 模板库 · 56 套精选</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="hero">
<div class="wrap">
<h1>Figma 模板库</h1>
<p class="sub">56 套 Web UI Kit · 35 套 Figma 原生 · 本地源包 + 预览 + 可投射到个人账户</p>
<div class="stats" id="stats"></div>
<div class="banner" id="banner"></div>
</div>
</header>
<nav class="toolbar">
<div class="wrap">
<div class="filters" id="filters">
<button class="pill active" data-filter="all">全部</button>
<button class="pill" data-filter="fig">Figma 原生</button>
<button class="pill" data-filter="sketch">Sketch</button>
<button class="pill" data-filter="xd">XD</button>
<button class="pill" data-filter="psd">PSD</button>
<button class="pill" data-filter="imported">已入 Figma</button>
</div>
<input type="search" id="search" placeholder="搜索模板名…">
</div>
</nav>
<main class="wrap">
<div class="grid" id="grid"></div>
<p class="empty" id="empty" hidden>没有匹配的模板</p>
</main>
<div class="modal" id="modal" hidden>
<div class="modal-bg"></div>
<div class="modal-panel">
<button class="modal-close" id="modalClose"></button>
<div class="modal-body" id="modalBody"></div>
</div>
</div>
<footer>
<div class="wrap">
<span>本地路径:<code>~/Projects/research/20260422-figma模板库/</code></span>
<span id="gen"></span>
</div>
</footer>
<script src="app.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Some files were not shown because too many files have changed in this diff Show More