202 lines
7.8 KiB
JavaScript
202 lines
7.8 KiB
JavaScript
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
function escapeAttr(s) {
|
||
return String(s).replace(/"/g, '"');
|
||
}
|
||
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();
|