Files
figma-templates-showcase/web/app.js
kang ee719d07cc 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>
2026-04-22 15:31:45 +08:00

174 lines
6.6 KiB
JavaScript
Raw 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.
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();