Files
2026-04-22 16:46:51 +08:00

202 lines
7.8 KiB
JavaScript
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.
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 variants = t.figma_variants || (t.figma_key ? [{name: t.name, key: t.figma_key, url: t.figma_url}] : []);
const hasMulti = variants.length > 1;
const tabsHtml = hasMulti
? `<div class="variant-tabs" id="variantTabs">
${variants.map((v, i) => `<button class="vtab ${i===0?'active':''}" data-idx="${i}">${escapeHtml(v.name || v.matched || `变体 ${i+1}`)}</button>`).join('')}
</div>`
: '';
const firstVariant = variants[0];
const iframeHtml = firstVariant
? `<iframe class="figma-embed" id="variantFrame" src="https://embed.figma.com/design/${firstVariant.key}/?embed-host=kang&footer=false" allowfullscreen></iframe>`
: '';
const openBtn = firstVariant
? `<a class="btn" id="variantOpenBtn" href="${firstVariant.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>`
: '');
body.innerHTML = `
<h2>${escapeHtml(t.name)}${hasMulti ? ` <span class="vcount">${variants.length} 变体</span>` : ''}</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">
${openBtn}
<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>
${tabsHtml}
${iframeHtml}
<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>
${firstVariant ? `<div class="k">Figma Key</div><div class="v" id="variantKey">${firstVariant.key}</div>` : ''}
</div>
<div class="gallery">
${(t.gallery||[]).map(g => `<img src="${g}" loading="lazy">`).join('')}
</div>
`;
if (hasMulti) {
document.getElementById('variantTabs').addEventListener('click', e => {
const btn = e.target.closest('.vtab');
if (!btn) return;
const idx = +btn.dataset.idx;
const v = variants[idx];
document.querySelectorAll('.vtab').forEach(b => b.classList.toggle('active', b === btn));
document.getElementById('variantFrame').src = `https://embed.figma.com/design/${v.key}/?embed-host=kang&footer=false`;
document.getElementById('variantOpenBtn').href = v.url;
document.getElementById('variantKey').textContent = v.key;
});
}
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();