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

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# keep repo lean — source archives + extracted are local only
source/
extracted/
.DS_Store
__pycache__/
*.pyc
.playwright-mcp/
# generated manifests stay in repo so site works on fresh clone
# (manifest.json, figma-files.json, figma-match-report.json, web/)

1
.htpasswd Normal file
View File

@@ -0,0 +1 @@
kang:$2y$10$J/3TiFUWxoSw4ahfmoQNmOPUarSDgf3gYFZzvheogHZbktACWFuxq

91
.memory/status.md Normal file
View File

@@ -0,0 +1,91 @@
# Figma 模板库 - 状态
> 2026-04-22 立项,从桌面搬迁 11GB "Figma模板网页官网首页PC电脑Web端UI界面Sketch Xd设计素材200套" 整理并展示
## 核心事实
- **"200 套" 名不副实**:实际 **56 个源包** + 202 张 A 预览索引缩略图
- 格式分布51×zip + 3×7z + 2×rar展开后
- **含 `.fig`**35 套Figma 原生)
-`.sketch`8 套
-`.xd`7 套
-`.psd`2 套
- 混合无 fig4 套
## 目录结构
```
~/Projects/research/20260422-figma模板库/
├── source/ 11G · 原始压缩包 + A 预览/ 202 张索引图
├── extracted/ 全 56 套已解压nested zip 也全递归解压了W27/W39/W20 等)
├── manifest.json 56 条元数据name/格式/文件列表/大小)
├── scripts/
│ └── import-to-figma.sh slim/full 模式批量 open -a Figma
├── web/ 展示站
│ ├── index.html
│ ├── styles.css
│ ├── app.js
│ ├── data.json 56 条含 cover/gallery/figma_key 字段key 暂空)
│ ├── thumbs/ 56 张 cover900px/jpg826.1M
│ └── previews/ 全部 galleries1600px/jpg8252M
└── .memory/
```
## 已写入 Figma Draftsslim 模式 43 个)
- W1 测试 1 个(手动 open+ 42 个 slim 批量
- **W37 Daily UI**30 天的 kit 只入了第 1 天("Day 03 - Videos Website Landing"),其他 29 天本地 `extracted/W37/` 保留
- W5 Wiloa 全 4 变体Hotel/Plant/Restaurant/Travel
- W56 Orabel 全 6 页Home/About/Portfolio/Blog/Contact/Open Menu
- 其他 32 个 template 各 1 个 fig
## 非 Figma 原生的 21 套(已搁置)
只 SketchW11, W12, W18, W20, W30, W446
只 XDW8, W19, W22, W23, W39, W496
只 PSDW17含 68 psd + 2 xd, W4211 psd
混合无 figW4, W9, W15, W21, W27, W36, W507
后续如需:
- Sketch → Figma 桌面 File→Import 可批量(多选)
- XD → 第三方转换器convertxd 之类)或搁置
- PSD → 非 Figma 生态
## 展示站
- 端口 **4010**(端口注册中心分的 4010-4019 块)
- 启动:`cd web && python3 -m http.server 4010`
- 功能:卡片网格 · 格式筛选fig/sketch/xd/psd/imported· 名称搜索 · lightbox modal · 本地源包路径复制 · Figma Drafts 跳转
- 暗色调 / CSS variables / 无框架 / 静态
## 活·iframe 投射 ✅ 已完成
**重大踩坑**`open -a Figma file.fig` 只在桌面版本地打开Untitled tab**不上传云端**。Drafts 云端 API 看不到。
**真·上传路径**Figma 网页版 Drafts 页 → "+" → Import → "Import from computer"(接受 .fig。Playwright `browser_file_upload` 可一次喂 42 个Figma 串行处理 ~50s/个 → 约 60 分钟全部上云。
**fileKey 抓取(绕了好几圈)**
1. ❌ Figma REST API无 "list drafts" 接口
2. ❌ Figma Desktop `settings.json`:不实时更新
3. ❌ AppleScript 读 Figma 桌面窗口:被系统拒
4. ❌ Figma Plugin API要已知 fileKey 才能进
5. ❌ Chromium CookiemacOS Keychain 加密
6.**Figma 内部 API**:登录后调 `/api/folders/{drafts_folder_id}/paginated_files?...` 拿全 listdrafts_folder_id 从 `/api/user/state` 拿不到,但触发 Drafts UI 后从 network 抓到 `194902307`
7. 模糊匹配名称回填 → `scripts/match-and-update.py`35/35 score 1.0
**最终架构**
- 静态卡片缩略图(本地 56 张,离线可看)
- 点开 modal → `<iframe src="https://embed.figma.com/design/{key}/?embed-host=kang&footer=false">` 实时渲染你 Figma Drafts 里的真实文件
- "在 Figma 打开 →" 按钮跳转编辑模式
- 35/35 fig 模板全部支持
iframe 嵌入只在你登录 Figma 的浏览器里能看drafts 私有),符合个人用途。
## 部署(未做)
若要上 VPS`styles.kang-kang.com` 那样):
- Coolify dockerfile nginx:alpine 模式
- DNS`*.kang-kang.com` 泛解析已指 76.13.31.179
- 建议域名:`figma-templates.kang-kang.com``figma.kang-kang.com`
- 本地静态文件体积web/ 合计 ~60MB可接受
- 加 basic-auth类似 styles 站)

9
Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY .htpasswd /etc/nginx/.htpasswd
COPY web/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

39
README.md Normal file
View File

@@ -0,0 +1,39 @@
# Figma 模板库
56 套 Web UI Kit 本地整理 + 静态展示站35 套 Figma 原生已云端就位 Drafts。
## 结构
```
├── source/ 11G · 原始 zip/7z/rar + 预览图gitignored
├── extracted/ 全套解压gitignored
├── manifest.json 56 条元数据
├── figma-files.json 云端 fileKey 列表
├── scripts/
│ ├── import-to-figma.sh open -a Figma 批量(注意:不上云,仅本地)
│ └── match-and-update.py 按名字模糊匹配 Figma fileKey 回填 data.json
├── web/ 静态展示站
│ ├── index.html / styles.css / app.js
│ ├── data.json 前端数据
│ ├── thumbs/ 6.1M 56 张 cover
│ └── previews/ 52M gallery 压缩 JPG
├── nginx.conf + .htpasswd + Dockerfile 部署用
└── .memory/status.md 项目记忆
```
## 本地跑
```bash
cd web && python3 -m http.server 4010
# → http://localhost:4010
```
## 部署
`nginx:alpine` 容器 + basic-authkang / 见 credentials.md
Coolify dockerfile 模式Gitea 主仓 + GitHub 镜像。
## 关键教训
- **`open -a Figma file.fig` 不上传云端**,仅本地打开。真·上云走网页 Import 对话框(接受 .fig .sketch .pdf 等)
- **fileKey 抓取**Figma REST 无 list-drafts用内部 API `/api/folders/{drafts_folder_id}/paginated_files` 需登录 cookiePlaywright MCP 实现

1
figma-files.json Normal file
View File

@@ -0,0 +1 @@
[{"key":"CLZepne3voPYx198q3wUHO","name":"Bright Kit"},{"key":"LA2daTQaigGkMUUNgPr6fp","name":"AKASHA-PAGEBUILDING-KIT"},{"key":"tLnL0G13yvkQtBZeQmPeq5","name":"06 Open Menu - Orabel Web UI Kit"},{"key":"bCrGhKu5PwOZh4F2EbyLk8","name":"05 Contact - Orabel Web UI Kit"},{"key":"Dc4uWhUeLCC6f2wiblXiBa","name":"04 Blog - Orabel Web UI Kit"},{"key":"BOnT7oN2pavJlDCdAO9Yih","name":"03 Portfolio - Orabel Web UI Kit"},{"key":"AEZMbGAy5IfnDAkFrbYwB6","name":"02 About - Orabel Web UI Kit"},{"key":"IoQBzPjHw51WjFKL52kJKN","name":"01 Home - Orabel Web UI Kit"},{"key":"IpdxPbiWuGZQpr3aNXsVpC","name":"Fecca"},{"key":"vcb4bLF79NTpybgXvAtkNM","name":"GETPAY - FINANCE TEMPLATES DESIGN"},{"key":"pMjAEiLKAFE57eaI4f4vva","name":"Aset - skill Shoot"},{"key":"U4YDJ7B0Sefem9r8LiEs8b","name":"Ui8_RealStatic_Static Mania"},{"key":"KtGIx2vrYNtApMiPYCTiiB","name":"Quicker Design system"},{"key":"3rhRDK0NC4oniMRGLNGnte","name":"WILOA - TRAVEL"},{"key":"1zIi1k19ngpoGixSB3upum","name":"WILOA - RESTAURANT"},{"key":"lv1hy5bS3Wx86slIXgorFn","name":"WILOA - PLANT"},{"key":"7C3QNOG6FhBRguT3nQ3dzf","name":"WILOA - HOTEL"},{"key":"df0oLsZta06a79s5iepJdF","name":"LANDINGKUY - Landing Page Design"},{"key":"Suvu5K5DSKPngkMKRb01MK","name":"Real Estate SaaS Kit & Dashboard (UI8)"},{"key":"sHmAQQxmvRTIxOg8OgQQFG","name":"3 template Agency"},{"key":"oZ7a2aFNZ21bg128ixnB3k","name":"Wiredunk_Wireframe"},{"key":"huDsMAoVThpYnGAU9j8WVB","name":"Modularity"},{"key":"4OTPxx5sgN8eFJ3joudoVH","name":"MasterFlow_UI_Kit"},{"key":"uAcopfnDXakv7l5Jr6sMqG","name":"iotask"},{"key":"fL1KqwIS3OmHCsUwhgJusG","name":"Figmaland- UI Kit (Startup)"},{"key":"PJHgFt29Lyo3JwMKxa5Tsm","name":"04_DailyUI_Video_Landing_Website"},{"key":"RNRNDiF6gFcTCi6wJncvgv","name":"JackCreative"},{"key":"BfBzvqu4VK53iSD5fbKNAo","name":"Intirior Architecture web ui kit"},{"key":"RKIYTIOCADVsASHNxZ5tr2","name":"Social Media Dashboard - UI Kit"},{"key":"qFC0pVEHe9Toc2aJ2MgO32","name":"DevAgency - Web Ui Kits"},{"key":"khWI1rrgwj5702NilEcha2","name":"Sydney Oasis Hotel Websites"},{"key":"5CZLD5lcxxRQNE1QWjqbmP","name":"UXFlow Web Kit"},{"key":"dvPWtG7KaEWr895iCI0Ioy","name":"LOOMI"},{"key":"5R9tevOwnk4iJzU3jEjg0q","name":"Landing UI Kit"},{"key":"oyHlh6eh7TjxYhCUbFTvBS","name":"Jobhuntly - Job Board & Portal UI Kit"},{"key":"G4mWQN0IWGkAKpD7Ga0bG3","name":"Core - Dashboard Builder"},{"key":"Sarco9c50gDttu37szmurE","name":"Webflew Agency UI Kit"},{"key":"x3g1FivshKeZ9PwccE7w3w","name":"Core - Dashboard Builder"},{"key":"fsufV596UmcNyM8yfCYlAr","name":"Eco -Ecommerce Analytics Admin Dashboard"},{"key":"s5KbowY6N6M4IkjHbgLjOc","name":"Figmaland-UI MAX ( ui8 )"},{"key":"ms5OYBa3fRlDwRmSLWWorY","name":"Online Shop UI Kit"},{"key":"pxsKzhDM8UL8oTSB4hUpOZ","name":"saascuy"},{"key":"EKp3ZDo2A3kDp88DMvFqte","name":"Havoc Agency UI Kit"}]

282
figma-match-report.json Normal file
View File

@@ -0,0 +1,282 @@
[
{
"W": "W1",
"name": "Saascuy - Saas Landing Page UI KIT",
"fig_stem": "saascuy",
"matched": "saascuy",
"key": "pxsKzhDM8UL8oTSB4hUpOZ",
"score": 1.0
},
{
"W": "W10",
"name": "Havoc Agency UI Kit",
"fig_stem": "Havoc Agency UI Kit",
"matched": "Havoc Agency UI Kit",
"key": "EKp3ZDo2A3kDp88DMvFqte",
"score": 1.0
},
{
"W": "W13",
"name": "Premium Online Shop UI Kit & Dashboard",
"fig_stem": "Online Shop UI Kit",
"matched": "Online Shop UI Kit",
"key": "ms5OYBa3fRlDwRmSLWWorY",
"score": 1.0
},
{
"W": "W14",
"name": "Ui Max",
"fig_stem": "Figmaland-UI MAX ( ui8 )",
"matched": "Figmaland-UI MAX ( ui8 )",
"key": "s5KbowY6N6M4IkjHbgLjOc",
"score": 1.0
},
{
"W": "W16",
"name": "Eco -Ecommerce Analytics Admin Dashboard Kit",
"fig_stem": "Eco -Ecommerce Analytics Admin Dashboard",
"matched": "Eco -Ecommerce Analytics Admin Dashboard",
"key": "fsufV596UmcNyM8yfCYlAr",
"score": 1.0
},
{
"W": "W2",
"name": "Core Dashboard Builder",
"fig_stem": "Core - Dashboard Builder",
"matched": "Core - Dashboard Builder",
"key": "G4mWQN0IWGkAKpD7Ga0bG3",
"score": 1.0
},
{
"W": "W24",
"name": "Webflew Agency Template UI Kit",
"fig_stem": "Webflew Agency UI Kit",
"matched": "Webflew Agency UI Kit",
"key": "Sarco9c50gDttu37szmurE",
"score": 1.0
},
{
"W": "W25",
"name": "Core Dashboard Builder",
"fig_stem": "Core - Dashboard Builder",
"matched": "Core - Dashboard Builder",
"key": "x3g1FivshKeZ9PwccE7w3w",
"score": 1.0
},
{
"W": "W26",
"name": "Jobhuntly - Job Board & Portal UI Kit",
"fig_stem": "Jobhuntly - Job Board & Portal UI Kit",
"matched": "Jobhuntly - Job Board & Portal UI Kit",
"key": "oyHlh6eh7TjxYhCUbFTvBS",
"score": 1.0
},
{
"W": "W28",
"name": "Finity - Landing Page Template",
"fig_stem": "Landing UI Kit",
"matched": "Landing UI Kit",
"key": "5R9tevOwnk4iJzU3jEjg0q",
"score": 1.0
},
{
"W": "W29",
"name": "Loomi",
"fig_stem": "LOOMI",
"matched": "LOOMI",
"key": "dvPWtG7KaEWr895iCI0Ioy",
"score": 1.0
},
{
"W": "W3",
"name": "UXFlow Web Kit Design",
"fig_stem": "UXFlow Web Kit",
"matched": "UXFlow Web Kit",
"key": "5CZLD5lcxxRQNE1QWjqbmP",
"score": 1.0
},
{
"W": "W31",
"name": "Sydney Oasis Hotel - Websites Template",
"fig_stem": "Sydney Oasis Hotel Websites",
"matched": "Sydney Oasis Hotel Websites",
"key": "khWI1rrgwj5702NilEcha2",
"score": 1.0
},
{
"W": "W32",
"name": "DevAgency - Web Ui Kits",
"fig_stem": "DevAgency - Web Ui Kits",
"matched": "DevAgency - Web Ui Kits",
"key": "qFC0pVEHe9Toc2aJ2MgO32",
"score": 1.0
},
{
"W": "W33",
"name": "Insight - Dashboard UI Kit",
"fig_stem": "Social Media Dashboard - UI Kit",
"matched": "Social Media Dashboard - UI Kit",
"key": "RKIYTIOCADVsASHNxZ5tr2",
"score": 1.0
},
{
"W": "W34",
"name": "Intirior Architecture web ui kit",
"fig_stem": "Intirior Architecture web ui kit",
"matched": "Intirior Architecture web ui kit",
"key": "BfBzvqu4VK53iSD5fbKNAo",
"score": 1.0
},
{
"W": "W35",
"name": "JackCreative Personal Portfolio Website Design",
"fig_stem": "JackCreative",
"matched": "JackCreative",
"key": "RNRNDiF6gFcTCi6wJncvgv",
"score": 1.0
},
{
"W": "W37",
"name": "Daily UI Starter Pages - A ready-made UI Kits",
"fig_stem": "04_DailyUI_Video_Landing_Website",
"matched": "04_DailyUI_Video_Landing_Website",
"key": "PJHgFt29Lyo3JwMKxa5Tsm",
"score": 1.0
},
{
"W": "W38",
"name": "Figmaland - Startup Ui Kit",
"fig_stem": "Figmaland- UI Kit (Startup)",
"matched": "Figmaland- UI Kit (Startup)",
"key": "fL1KqwIS3OmHCsUwhgJusG",
"score": 1.0
},
{
"W": "W40",
"name": "IOTASK UI Kit",
"fig_stem": "iotask",
"matched": "iotask",
"key": "uAcopfnDXakv7l5Jr6sMqG",
"score": 1.0
},
{
"W": "W41",
"name": "Master Flow - Responsive Template for Figma",
"fig_stem": "MasterFlow_UI_Kit",
"matched": "MasterFlow_UI_Kit",
"key": "4OTPxx5sgN8eFJ3joudoVH",
"score": 1.0
},
{
"W": "W43",
"name": "Modularity Web Design System for Figma",
"fig_stem": "Modularity",
"matched": "Modularity",
"key": "huDsMAoVThpYnGAU9j8WVB",
"score": 1.0
},
{
"W": "W45",
"name": "Wiredunk - Landing Page Template for Multipurposes",
"fig_stem": "Wiredunk_Wireframe",
"matched": "Wiredunk_Wireframe",
"key": "oZ7a2aFNZ21bg128ixnB3k",
"score": 1.0
},
{
"W": "W46",
"name": "agency website template",
"fig_stem": "3 template Agency",
"matched": "3 template Agency",
"key": "sHmAQQxmvRTIxOg8OgQQFG",
"score": 1.0
},
{
"W": "W47",
"name": "Estatery - Real Estate SaaS Web UI Kit",
"fig_stem": "Real Estate SaaS Kit & Dashboard (UI8)",
"matched": "Real Estate SaaS Kit & Dashboard (UI8)",
"key": "Suvu5K5DSKPngkMKRb01MK",
"score": 1.0
},
{
"W": "W48",
"name": "LANDINGKUY - Landing Page Design Templates",
"fig_stem": "LANDINGKUY - Landing Page Design",
"matched": "LANDINGKUY - Landing Page Design",
"key": "df0oLsZta06a79s5iepJdF",
"score": 1.0
},
{
"W": "W5",
"name": "Wiloa 2.0 - Landing Page UI-Kit",
"fig_stem": "WILOA - HOTEL",
"matched": "WILOA - HOTEL",
"key": "7C3QNOG6FhBRguT3nQ3dzf",
"score": 1.0
},
{
"W": "W51",
"name": "Quickr Design System",
"fig_stem": "Quicker Design system",
"matched": "Quicker Design system",
"key": "KtGIx2vrYNtApMiPYCTiiB",
"score": 1.0
},
{
"W": "W52",
"name": "RealStatic - Real State Website Design",
"fig_stem": "Ui8_RealStatic_Static Mania",
"matched": "Ui8_RealStatic_Static Mania",
"key": "U4YDJ7B0Sefem9r8LiEs8b",
"score": 1.0
},
{
"W": "W53",
"name": "Skill Shoot - Online course website and responsive uikit",
"fig_stem": "Aset - skill Shoot",
"matched": "Aset - skill Shoot",
"key": "pMjAEiLKAFE57eaI4f4vva",
"score": 1.0
},
{
"W": "W54",
"name": "GetPay - 6 Unique Finance Landing Pages",
"fig_stem": "GETPAY - FINANCE TEMPLATES DESIGN",
"matched": "GETPAY - FINANCE TEMPLATES DESIGN",
"key": "vcb4bLF79NTpybgXvAtkNM",
"score": 1.0
},
{
"W": "W55",
"name": "Fecca - Landing Page UI Kit (Figma)",
"fig_stem": "Fecca",
"matched": "Fecca",
"key": "IpdxPbiWuGZQpr3aNXsVpC",
"score": 1.0
},
{
"W": "W56",
"name": "Orabel Web UI Kit",
"fig_stem": "01 Home - Orabel Web UI Kit",
"matched": "01 Home - Orabel Web UI Kit",
"key": "IoQBzPjHw51WjFKL52kJKN",
"score": 1.0
},
{
"W": "W6",
"name": "AKASHA Pages Building Kit",
"fig_stem": "AKASHA-PAGEBUILDING-KIT",
"matched": "AKASHA-PAGEBUILDING-KIT",
"key": "LA2daTQaigGkMUUNgPr6fp",
"score": 1.0
},
{
"W": "W7",
"name": "Bright Kit Web Layouts",
"fig_stem": "Bright Kit",
"matched": "Bright Kit",
"key": "CLZepne3voPYx198q3wUHO",
"score": 1.0
}
]

1794
manifest.json Normal file

File diff suppressed because it is too large Load Diff

39
nginx.conf Normal file
View File

@@ -0,0 +1,39 @@
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
server {
listen 80 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;
auth_basic "Figma 模板库";
auth_basic_user_file /etc/nginx/.htpasswd;
# cache immutable assets
location ~* \.(jpg|jpeg|png|gif|webp|svg|woff2?|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location ~* \.(css|js)$ {
expires 7d;
}
location ~* \.json$ {
add_header Cache-Control "no-cache";
}
location / {
try_files $uri $uri/ =404;
}
}
}

41
scripts/import-to-figma.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Batch open .fig files in Figma desktop app (→ Drafts)
# Mode: "all" (72 files) or "slim" (W37 only 1, rest all = 48 files)
set -euo pipefail
MODE="${1:-slim}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
python3 - "$MODE" <<'PY' > /tmp/fig-list.txt
import json, sys, os
mode = sys.argv[1]
m = json.load(open("manifest.json"))
already_opened = {"W1"} # already done in earlier test
files = []
for t in m["templates"]:
if not t["fig"]: continue
figs = list(t["fig"])
if mode == "slim" and t["id"] == "W37":
figs = figs[:1]
for f in figs:
path = f'extracted/{t["id"]}/{f}'
if t["id"] in already_opened and figs.index(f) == 0 and t["id"] == "W1":
continue # skip W1 first fig (already opened)
files.append(path)
print('\n'.join(files))
PY
count=$(wc -l < /tmp/fig-list.txt | tr -d ' ')
echo "Mode=$MODE Files to open: $count"
echo ""
i=0
while IFS= read -r f; do
i=$((i+1))
printf "[%2d/%d] %s\n" "$i" "$count" "$f"
open -a Figma "$ROOT/$f"
sleep 1.5
done < /tmp/fig-list.txt
echo ""
echo "Done. All $count .fig files sent to Figma."

105
scripts/match-and-update.py Executable file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Match Figma cloud Drafts files (from figma-files.json) back to W{N} entries in
manifest.json by fuzzy name match, then update web/data.json with figma_key+url.
"""
import json, re, sys, difflib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def normalize(s):
s = s.lower()
s = re.sub(r'[^a-z0-9]+', ' ', s).strip()
return s
def best_match(target, candidates):
"""Return (score, candidate) best match from candidates list of dicts with .name"""
nt = normalize(target)
best = (0, None)
for c in candidates:
nc = normalize(c['name'])
# exact / prefix / contains / sequence
if nt == nc: s = 1.0
elif nt in nc or nc in nt: s = 0.85
else: s = difflib.SequenceMatcher(None, nt, nc).ratio()
if s > best[0]:
best = (s, c)
return best
def main():
manifest = json.loads((ROOT/'manifest.json').read_text())
figma_files_path = ROOT/'figma-files.json'
if not figma_files_path.exists():
print(f"missing {figma_files_path}", file=sys.stderr)
sys.exit(1)
figma_files = json.loads(figma_files_path.read_text()) # list of {key, name, ...}
# for each W template that has fig, find matching figma file
matches = []
used_keys = set()
for t in manifest['templates']:
if not t['fig']: continue
# use the .fig file's stem as match target (closer than archive name)
targets = [Path(f).stem for f in t['fig']]
# add archive stem as backup
if t.get('archive'):
targets.append(Path(t['archive']).stem)
targets.append(t['name'])
# try each target
best_overall = (0, None)
for tgt in targets:
score, cand = best_match(tgt, [f for f in figma_files if f['key'] not in used_keys])
if score > best_overall[0]:
best_overall = (score, cand)
if score > 0.95:
break
score, cand = best_overall
if cand and score >= 0.6:
used_keys.add(cand['key'])
matches.append({
'W': t['id'], 'name': t['name'], 'fig_stem': Path(t['fig'][0]).stem if t['fig'] else None,
'matched': cand['name'], 'key': cand['key'], 'score': round(score, 3)
})
else:
matches.append({
'W': t['id'], 'name': t['name'], 'fig_stem': Path(t['fig'][0]).stem if t['fig'] else None,
'matched': None, 'best_score': round(best_overall[0], 3) if cand else 0,
'best_candidate': cand['name'] if cand else None
})
# Update web/data.json
data_path = ROOT/'web'/'data.json'
data = json.loads(data_path.read_text())
by_W = {m['W']: m for m in matches if m.get('key')}
for t in data['templates']:
m = by_W.get(t['id'])
if m:
t['figma_key'] = m['key']
t['figma_url'] = f"https://www.figma.com/file/{m['key']}"
else:
t['figma_key'] = None
t['figma_url'] = None
# update banner with imported count
imported = sum(1 for t in data['templates'] if t['figma_key'])
data['imported_summary'] = (
f"✅ <b>{imported} 个 Figma 原生文件</b>已云端就位在你的 "
f"<a href='https://www.figma.com/files/team/1304178887825899477/drafts' target='_blank'>Figma Drafts</a>。"
f"点卡片打开 modal 查看 iframe 实时投射 + 跳 Figma 编辑。"
)
data_path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
# write match report
report = ROOT/'figma-match-report.json'
report.write_text(json.dumps(matches, ensure_ascii=False, indent=2))
print(f"Matched {imported}/{sum(1 for t in manifest['templates'] if t['fig'])} fig templates")
print(f"Report: {report.relative_to(ROOT)}")
unmatched = [m for m in matches if not m.get('key')]
if unmatched:
print(f"\n⚠️ {len(unmatched)} unmatched:")
for m in unmatched:
print(f" {m['W']:4s} {m['name'][:50]:50s} best='{m.get('best_candidate','')[:40]}' score={m.get('best_score',0)}")
if __name__ == '__main__':
main()

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

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