@code2rich/jpage 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +3 -1
- package/.github/workflows/release.yml +87 -0
- package/CLAUDE.md +3 -1
- package/README.md +26 -2
- package/bin/commands/ls.js +6 -3
- package/bin/commands/update.js +74 -0
- package/bin/jpage.js +7 -2
- package/docs/RELEASING.md +209 -0
- package/docs/skill-integration-design.md +384 -0
- package/eslint.config.mjs +2 -0
- package/lib/csp.js +8 -2
- package/lib/render.js +9 -2
- package/lib/templates.js +1 -1
- package/mcp/tools-files.js +5 -1
- package/package.json +4 -4
- package/public/css/style.css +128 -1
- package/public/index.html +51 -3
- package/public/js/app.js +8 -6
- package/public/js/pages/content-templates.js +1 -1
- package/public/js/pages/home.js +218 -9
- package/public/js/pages/landing.js +1 -1
- package/public/js/pages/preview.js +1 -1
- package/public/js/utils.js +15 -7
- package/routes/skills.js +77 -3
- package/server.js +10 -3
- package/skills/jpage-presentation/INSTALL.md +50 -0
- package/skills/jpage-presentation/README.md +71 -0
- package/skills/jpage-presentation/SKILL.md +226 -0
- package/skills/jpage-presentation/assets/plugin/highlight/monokai.css +71 -0
- package/skills/jpage-presentation/assets/plugin/highlight/plugin.js +439 -0
- package/skills/jpage-presentation/assets/plugin/notes/notes.js +1 -0
- package/skills/jpage-presentation/assets/reveal-base.css +9 -0
- package/skills/jpage-presentation/assets/reveal.js +9 -0
- package/skills/jpage-presentation/assets/themes/academic.css +68 -0
- package/skills/jpage-presentation/assets/themes/business.css +64 -0
- package/skills/jpage-presentation/assets/themes/creative.css +81 -0
- package/skills/jpage-presentation/assets/themes/minimal.css +117 -0
- package/skills-registry.js +0 -6
- package/test/dispatch-bench.js +0 -3
- package/test/integration/cli.test.js +93 -0
- package/test/integration/skills.test.js +27 -5
- package/test/perf-harness.js +0 -9
- package/test/unit/fts.test.js +0 -1
- package/.claude/settings.local.json +0 -68
package/public/css/style.css
CHANGED
|
@@ -236,7 +236,10 @@ html.light .theme-toggle .icon-sun { display: block; }
|
|
|
236
236
|
color: #fff;
|
|
237
237
|
border-color: var(--primary);
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
/* 图标按钮(btn-small 内仅放 SVG):方形、与星标/更多按钮视觉一致 */
|
|
240
|
+
.btn-icon { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; padding: 0; color: var(--text-secondary); }
|
|
241
|
+
.btn-icon svg { width: 16px; height: 16px; }
|
|
242
|
+
.btn-copy-link:hover { color: var(--primary); }
|
|
240
243
|
.btn-copy-link:hover { background: var(--primary); color: #fff; }
|
|
241
244
|
|
|
242
245
|
.auth-message {
|
|
@@ -1350,6 +1353,10 @@ html:not(.light) {
|
|
|
1350
1353
|
.skeleton-w60 { width: 60%; }
|
|
1351
1354
|
.skeleton-w40 { width: 40%; }
|
|
1352
1355
|
|
|
1356
|
+
.skeleton-card { display: block; padding: 0; }
|
|
1357
|
+
.skeleton-card-thumb { height: 160px; margin-bottom: 12px; border-radius: 6px 6px 0 0; background: linear-gradient(90deg, var(--border) 25%, rgba(0,0,0,.06) 50%, var(--border) 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; }
|
|
1358
|
+
.skeleton-card .skeleton-line { margin: 0 14px 8px; }
|
|
1359
|
+
|
|
1353
1360
|
@keyframes shimmer {
|
|
1354
1361
|
0% { background-position: 200% 0; }
|
|
1355
1362
|
100% { background-position: -200% 0; }
|
|
@@ -2317,6 +2324,85 @@ html:not(.light) {
|
|
|
2317
2324
|
font-weight: 600;
|
|
2318
2325
|
}
|
|
2319
2326
|
|
|
2327
|
+
/* CLI tab:渲染后的 Markdown 文档(标题/段落/列表/代码块/表格) */
|
|
2328
|
+
.mcp-cli-doc {
|
|
2329
|
+
font-size: 13px;
|
|
2330
|
+
line-height: 1.6;
|
|
2331
|
+
color: var(--text);
|
|
2332
|
+
}
|
|
2333
|
+
.mcp-cli-doc h1 {
|
|
2334
|
+
font-size: 18px;
|
|
2335
|
+
margin: 0 0 12px;
|
|
2336
|
+
}
|
|
2337
|
+
.mcp-cli-doc h2 {
|
|
2338
|
+
font-size: 14px;
|
|
2339
|
+
margin: 18px 0 8px;
|
|
2340
|
+
color: var(--text);
|
|
2341
|
+
}
|
|
2342
|
+
.mcp-cli-doc p {
|
|
2343
|
+
margin: 0 0 10px;
|
|
2344
|
+
}
|
|
2345
|
+
.mcp-cli-doc ul,
|
|
2346
|
+
.mcp-cli-doc ol {
|
|
2347
|
+
margin: 0 0 10px;
|
|
2348
|
+
padding-left: 20px;
|
|
2349
|
+
}
|
|
2350
|
+
.mcp-cli-doc li {
|
|
2351
|
+
margin-bottom: 4px;
|
|
2352
|
+
}
|
|
2353
|
+
.mcp-cli-doc blockquote {
|
|
2354
|
+
margin: 0 0 10px;
|
|
2355
|
+
padding: 8px 12px;
|
|
2356
|
+
border-left: 3px solid var(--primary);
|
|
2357
|
+
background: var(--bg);
|
|
2358
|
+
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
2359
|
+
color: var(--text-secondary);
|
|
2360
|
+
}
|
|
2361
|
+
.mcp-cli-doc blockquote p {
|
|
2362
|
+
margin: 0;
|
|
2363
|
+
}
|
|
2364
|
+
.mcp-cli-doc code {
|
|
2365
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2366
|
+
font-size: 12px;
|
|
2367
|
+
background: var(--bg);
|
|
2368
|
+
padding: 1px 5px;
|
|
2369
|
+
border-radius: 3px;
|
|
2370
|
+
}
|
|
2371
|
+
/* 代码块(非行内)复用 .mcp-config-code 的样式,保持视觉一致 */
|
|
2372
|
+
.mcp-cli-doc pre {
|
|
2373
|
+
background: var(--bg);
|
|
2374
|
+
border-radius: var(--radius-sm);
|
|
2375
|
+
padding: 12px 14px;
|
|
2376
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2377
|
+
font-size: 12px;
|
|
2378
|
+
line-height: 1.55;
|
|
2379
|
+
white-space: pre-wrap;
|
|
2380
|
+
word-break: break-word;
|
|
2381
|
+
margin: 0 0 10px;
|
|
2382
|
+
overflow-x: auto;
|
|
2383
|
+
}
|
|
2384
|
+
.mcp-cli-doc pre code {
|
|
2385
|
+
background: none;
|
|
2386
|
+
padding: 0;
|
|
2387
|
+
}
|
|
2388
|
+
.mcp-cli-doc table {
|
|
2389
|
+
width: 100%;
|
|
2390
|
+
border-collapse: collapse;
|
|
2391
|
+
margin: 0 0 12px;
|
|
2392
|
+
font-size: 12px;
|
|
2393
|
+
}
|
|
2394
|
+
.mcp-cli-doc th,
|
|
2395
|
+
.mcp-cli-doc td {
|
|
2396
|
+
border: 1px solid var(--border);
|
|
2397
|
+
padding: 6px 10px;
|
|
2398
|
+
text-align: left;
|
|
2399
|
+
vertical-align: top;
|
|
2400
|
+
}
|
|
2401
|
+
.mcp-cli-doc th {
|
|
2402
|
+
background: var(--bg);
|
|
2403
|
+
font-weight: 600;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2320
2406
|
html:not(.light) {
|
|
2321
2407
|
.mcp-status-on {
|
|
2322
2408
|
background: rgba(22, 163, 74, 0.15);
|
|
@@ -2716,6 +2802,43 @@ html:not(.light) {
|
|
|
2716
2802
|
.ct-card-thumb { background: linear-gradient(135deg, #1e293b, #334155); }
|
|
2717
2803
|
}
|
|
2718
2804
|
|
|
2805
|
+
/* --- 文件列表视图切换 / 卡片视图 --- */
|
|
2806
|
+
.view-toggle { display: flex; gap: 2px; margin-left: auto; background: var(--bg-secondary, #f1f5f9); border: 1px solid var(--border); border-radius: 999px; padding: 2px; }
|
|
2807
|
+
.view-toggle-btn { min-width: 30px; height: 28px; padding: 0 10px; border: none; border-radius: 999px; background: transparent; color: var(--text-secondary); font-size: 16px; line-height: 1; cursor: pointer; transition: background .15s, color .15s; display: inline-flex; align-items: center; justify-content: center; }
|
|
2808
|
+
.view-toggle-btn:hover { color: var(--text); }
|
|
2809
|
+
.view-toggle-btn.active { background: var(--card); color: var(--primary); box-shadow: 0 1px 3px var(--shadow); }
|
|
2810
|
+
|
|
2811
|
+
.file-list.view-card { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
|
|
2812
|
+
|
|
2813
|
+
.file-card { position: relative; background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 14px; cursor: pointer; transition: box-shadow .2s, border-color .2s; display: flex; flex-direction: column; gap: 8px; }
|
|
2814
|
+
.file-card:hover { box-shadow: 0 2px 8px var(--shadow); border-color: var(--primary); }
|
|
2815
|
+
.file-card.selected { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary); }
|
|
2816
|
+
.file-card-thumb { height: 160px; position: relative; overflow: hidden; background: linear-gradient(135deg, var(--bg-secondary, #f1f5f9), var(--border)); margin: -14px -14px 8px; border-radius: 6px 6px 0 0; }
|
|
2817
|
+
.file-card-thumb-wrap { width: 1024px; height: 640px; transform: scale(0.25); transform-origin: top left; pointer-events: none; opacity: 0; transition: opacity .25s; }
|
|
2818
|
+
.file-card-thumb-wrap.loaded { opacity: 1; }
|
|
2819
|
+
.file-card-thumb-iframe { width: 100%; height: 100%; border: none; background: #fff; }
|
|
2820
|
+
.file-card-thumb-loading { position: absolute; inset: 0; background: linear-gradient(135deg, var(--bg-secondary, #f1f5f9), var(--border)); animation: ct-thumb-pulse 1.5s ease-in-out infinite; }
|
|
2821
|
+
.file-card-icon-btn { position: absolute; top: 6px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 999px; background: rgba(255,255,255,.85); color: var(--text-secondary); font-size: 14px; cursor: pointer; z-index: 2; transition: background .15s, color .15s; }
|
|
2822
|
+
.file-card-icon-btn svg { width: 15px; height: 15px; }
|
|
2823
|
+
.file-card-star { right: 6px; } /* 星标靠最右 */
|
|
2824
|
+
.file-card-copy { right: 40px; } /* 复制链接在星标左侧(28 宽 + 6 间距 + 6 边距)*/
|
|
2825
|
+
.file-card-star.starred { color: #f59e0b; }
|
|
2826
|
+
.file-card-icon-btn:hover { background: #fff; color: var(--text); }
|
|
2827
|
+
.file-card-star.starred:hover { color: #f59e0b; }
|
|
2828
|
+
.file-card-name { font-size: 14px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.4; word-break: break-all; }
|
|
2829
|
+
.file-card-badges { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
2830
|
+
.file-card-badges .file-badge { font-size: 10px; padding: 1px 6px; line-height: 1.5; }
|
|
2831
|
+
.file-card-footer { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--text-secondary); margin-top: auto; }
|
|
2832
|
+
html:not(.light) {
|
|
2833
|
+
.file-card-thumb { background: linear-gradient(135deg, #1e293b, #334155); }
|
|
2834
|
+
.file-card-icon-btn { background: rgba(30,41,59,.85); color: #94a3b8; }
|
|
2835
|
+
.file-card-icon-btn:hover { background: #334155; color: #e2e8f0; }
|
|
2836
|
+
.file-card-star.starred { color: #fbbf24; }
|
|
2837
|
+
}
|
|
2838
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2839
|
+
.file-card-thumb-loading { animation: none; }
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2719
2842
|
/* --- 落地页 --- */
|
|
2720
2843
|
.landing-page { min-height: 100vh; background: var(--bg); display: flex; flex-direction: column; }
|
|
2721
2844
|
.landing-nav { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: transparent; transition: background .3s, border-color .3s, box-shadow .3s; border-bottom: 1px solid transparent; }
|
|
@@ -2823,17 +2946,21 @@ html:not(.light) {
|
|
|
2823
2946
|
.ver:nth-child(3):hover .ver-dot { background: #06b6d4; box-shadow: 0 0 18px rgba(6,182,212,.5); }
|
|
2824
2947
|
.ver:nth-child(4) .ver-dot { border-color: #f59e0b; }
|
|
2825
2948
|
.ver:nth-child(4):hover .ver-dot { background: #f59e0b; box-shadow: 0 0 18px rgba(245,158,11,.5); }
|
|
2949
|
+
.ver:nth-child(5) .ver-dot { border-color: #10b981; }
|
|
2950
|
+
.ver:nth-child(5):hover .ver-dot { background: #10b981; box-shadow: 0 0 18px rgba(16,185,129,.5); }
|
|
2826
2951
|
.ver-card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 28px 28px 24px; backdrop-filter: blur(8px); transition: all .4s; }
|
|
2827
2952
|
.ver:hover .ver-card { border-color: var(--primary); box-shadow: var(--glow); }
|
|
2828
2953
|
.ver:nth-child(2):hover .ver-card { border-color: #8b5cf6; box-shadow: 0 0 24px rgba(139,92,246,.2); }
|
|
2829
2954
|
.ver:nth-child(3):hover .ver-card { border-color: #06b6d4; box-shadow: 0 0 24px rgba(6,182,212,.2); }
|
|
2830
2955
|
.ver:nth-child(4):hover .ver-card { border-color: #f59e0b; box-shadow: 0 0 24px rgba(245,158,11,.2); }
|
|
2956
|
+
.ver:nth-child(5):hover .ver-card { border-color: #10b981; box-shadow: 0 0 24px rgba(16,185,129,.2); }
|
|
2831
2957
|
.ver-head { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
2832
2958
|
.ver-num { font-family: ui-monospace, "SF Mono", monospace; font-size: 13px; font-weight: 700; letter-spacing: 1px; padding: 3px 10px; border-radius: 6px; }
|
|
2833
2959
|
.ver:nth-child(1) .ver-num { color: var(--primary); background: rgba(99,102,241,.12); }
|
|
2834
2960
|
.ver:nth-child(2) .ver-num { color: #8b5cf6; background: rgba(139,92,246,.10); }
|
|
2835
2961
|
.ver:nth-child(3) .ver-num { color: #06b6d4; background: rgba(6,182,212,.10); }
|
|
2836
2962
|
.ver:nth-child(4) .ver-num { color: #f59e0b; background: rgba(245,158,11,.10); }
|
|
2963
|
+
.ver:nth-child(5) .ver-num { color: #10b981; background: rgba(16,185,129,.10); }
|
|
2837
2964
|
.ver-date { font-family: ui-monospace, "SF Mono", monospace; font-size: 12px; color: var(--text-secondary); }
|
|
2838
2965
|
.ver-title { font-size: 20px; font-weight: 700; flex: 1; }
|
|
2839
2966
|
.ver-theme { font-size: 13px; color: var(--text-secondary); font-style: italic; padding: 2px 10px; border-radius: 4px; background: rgba(255,255,255,.04); }
|
package/public/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<meta name="description" content="即页,拖入 HTML 或 Markdown 文件,即刻获得在线预览页面。">
|
|
7
7
|
<title>即页 — 拖入文件,即刻成页</title>
|
|
8
|
-
<link rel="stylesheet" href="/css/style.css?v=1.
|
|
8
|
+
<link rel="stylesheet" href="/css/style.css?v=1.6.2">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app"></div>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
<section class="landing-sec" id="versions">
|
|
53
53
|
<div class="landing-sec-inner">
|
|
54
54
|
<div class="landing-sec-tag">Version History</div>
|
|
55
|
-
<h2 class="landing-sec-title"
|
|
55
|
+
<h2 class="landing-sec-title">五次进化,五个问题的答案</h2>
|
|
56
56
|
<div class="landing-versions">
|
|
57
57
|
<div class="ver">
|
|
58
58
|
<div class="ver-dot"></div>
|
|
@@ -135,6 +135,27 @@
|
|
|
135
135
|
</div>
|
|
136
136
|
</div>
|
|
137
137
|
</div>
|
|
138
|
+
<div class="ver">
|
|
139
|
+
<div class="ver-dot"></div>
|
|
140
|
+
<div class="ver-card">
|
|
141
|
+
<div class="ver-head">
|
|
142
|
+
<span class="ver-num">V1.5</span>
|
|
143
|
+
<span class="ver-date">2026-06-19</span>
|
|
144
|
+
<span class="ver-title">双客户端 + AI 演示 + 卡片视图</span>
|
|
145
|
+
<span class="ver-theme">「快速、多样、直观地发布」</span>
|
|
146
|
+
</div>
|
|
147
|
+
<p class="ver-desc">jpage CLI 成为与 MCP 并列的第二客户端入口——大文件 multipart 流式上传,告别 base64。新增 jpage-presentation Skill,AI 一句话生成 reveal.js 幻灯片。文件列表卡片视图用 iframe 实时缩略图,所见即所得。</p>
|
|
148
|
+
<div class="ver-highlights">
|
|
149
|
+
<div class="hl"><span class="hl-icon">⌨️</span>jpage CLI(11 命令 · npm 包)</div>
|
|
150
|
+
<div class="hl"><span class="hl-icon">🎞️</span>jpage-presentation 幻灯片 Skill</div>
|
|
151
|
+
<div class="hl"><span class="hl-icon">🃏</span>卡片视图 iframe 实时缩略图</div>
|
|
152
|
+
<div class="hl"><span class="hl-icon">🎨</span>reveal.js 4 主题(商务/学术/创意/极简)</div>
|
|
153
|
+
<div class="hl"><span class="hl-icon">🔗</span>复制链接改为图标按钮</div>
|
|
154
|
+
<div class="hl"><span class="hl-icon">⚡</span>multipart 流式上传(CLI)</div>
|
|
155
|
+
<div class="hl"><span class="hl-icon">🛠️</span>双客户端入口拆分 + 缓存刷新修复</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
138
159
|
</div>
|
|
139
160
|
</div>
|
|
140
161
|
</section>
|
|
@@ -268,6 +289,10 @@ Skills 管理
|
|
|
268
289
|
<button type="button" class="settings-menu-item" role="menuitem" id="menu-item-mcp">
|
|
269
290
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
270
291
|
MCP 配置
|
|
292
|
+
</button>
|
|
293
|
+
<button type="button" class="settings-menu-item" role="menuitem" id="menu-item-cli">
|
|
294
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
|
|
295
|
+
CLI 工具
|
|
271
296
|
</button>
|
|
272
297
|
<div class="settings-menu-divider" role="separator"></div>
|
|
273
298
|
<button type="button" class="settings-menu-item" role="menuitem" id="menu-item-tokens">
|
|
@@ -350,6 +375,10 @@ MCP 配置
|
|
|
350
375
|
<div class="filter-dropdown-menu" id="category-filter-menu"></div>
|
|
351
376
|
</div>
|
|
352
377
|
</div>
|
|
378
|
+
<div class="view-toggle" role="tablist" aria-label="展示形式">
|
|
379
|
+
<button type="button" class="view-toggle-btn active" data-view="list" role="tab" title="列表视图" aria-label="列表视图">≡</button>
|
|
380
|
+
<button type="button" class="view-toggle-btn" data-view="card" role="tab" title="卡片视图" aria-label="卡片视图">▦</button>
|
|
381
|
+
</div>
|
|
353
382
|
</div>
|
|
354
383
|
<div class="file-list" id="file-list" aria-busy="false"></div>
|
|
355
384
|
<div class="empty-state" id="empty-state" style="display:none">
|
|
@@ -518,6 +547,25 @@ MCP 配置
|
|
|
518
547
|
</div>
|
|
519
548
|
</div>
|
|
520
549
|
|
|
550
|
+
<div class="modal-backdrop" id="cli-config-modal" hidden aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="cli-config-title">
|
|
551
|
+
<div class="modal-panel">
|
|
552
|
+
<div class="modal-header">
|
|
553
|
+
<h2 id="cli-config-title">jpage CLI</h2>
|
|
554
|
+
<button type="button" class="btn btn-small modal-close" id="cli-config-close" aria-label="关闭">×</button>
|
|
555
|
+
</div>
|
|
556
|
+
<div class="modal-body">
|
|
557
|
+
<p class="mcp-config-hint"><code>jpage</code> 是即页的命令行工具,与 MCP 是并列的两个客户端入口(都架在同一套 REST API 上)。有 Bash 的场景(Claude Code、脚本、CI)用 CLI 上传更快更省。</p>
|
|
558
|
+
<div class="mcp-config-block">
|
|
559
|
+
<button type="button" class="btn btn-small mcp-copy-btn" id="cli-copy-guide">复制文档</button>
|
|
560
|
+
<div class="mcp-cli-doc cli-detail" id="cli-detail">加载中…</div>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="modal-footer">
|
|
564
|
+
<button type="button" class="btn btn-small" id="cli-config-dismiss">关闭</button>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
521
569
|
<!-- Skills 列表弹窗 -->
|
|
522
570
|
<div class="modal-backdrop" id="skills-list-modal" hidden aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="skills-list-title">
|
|
523
571
|
<div class="modal-panel modal-panel-wide">
|
|
@@ -850,6 +898,6 @@ MCP 配置
|
|
|
850
898
|
</div>
|
|
851
899
|
</div>
|
|
852
900
|
|
|
853
|
-
<script type="module" src="/js/app.js?v=1.
|
|
901
|
+
<script type="module" src="/js/app.js?v=1.5.3"></script>
|
|
854
902
|
</body>
|
|
855
903
|
</html>
|
package/public/js/app.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { api } from './api.js';
|
|
7
7
|
import { dialogModal } from './components/dialog.js';
|
|
8
|
-
import { toast } from './components/toast.js';
|
|
9
8
|
import { initTheme, setupThemeToggle } from './theme.js';
|
|
10
9
|
|
|
11
10
|
const state = {
|
|
@@ -32,10 +31,13 @@ function navigate(path) {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
// 动态加载各路由模块(esbuild 据此做代码分割,产出独立 chunk)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
async function
|
|
34
|
+
// 动态 import 带字面量版本串:发版时连同 index.html 里 app.js 的 ?v= 一起改,
|
|
35
|
+
// 让浏览器在开发模式(直接服务源文件、无内容哈希文件名)下也重新拉取各页面 chunk,
|
|
36
|
+
// 绕过 immutable 长缓存。字面量(非模板)能被 esbuild 正确分割。
|
|
37
|
+
async function loadHome() { const m = await import('./pages/home.js?v=1.5.2'); return m.renderHome; }
|
|
38
|
+
async function loadLogin() { const m = await import('./pages/login.js?v=1.5.2'); return m.renderLogin; }
|
|
39
|
+
async function loadLanding() { const m = await import('./pages/landing.js?v=1.5.2'); return m.renderLanding; }
|
|
40
|
+
async function loadPreview() { const m = await import('./pages/preview.js?v=1.5.2'); return m.renderPreview; }
|
|
39
41
|
|
|
40
42
|
function route() {
|
|
41
43
|
const hash = location.hash.replace('#', '') || '/';
|
|
@@ -79,7 +81,7 @@ function route() {
|
|
|
79
81
|
} else if (hash === '/register') {
|
|
80
82
|
loadLogin().then((renderLogin) => { renderLogin(appEl, 'register'); setupThemeToggle(appEl); });
|
|
81
83
|
} else {
|
|
82
|
-
loadLanding().then((renderLanding) => { renderLanding(appEl
|
|
84
|
+
loadLanding().then((renderLanding) => { renderLanding(appEl); setupThemeToggle(appEl); });
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 内容模板市场:浏览、上传、详情
|
|
2
2
|
|
|
3
|
-
import { api
|
|
3
|
+
import { api } from '../api.js';
|
|
4
4
|
import { toast } from '../components/toast.js';
|
|
5
5
|
import { dialogModal } from '../components/dialog.js';
|
|
6
6
|
import { escapeHtml, relativeTime, openModal, closeModal } from '../utils.js';
|
package/public/js/pages/home.js
CHANGED
|
@@ -16,9 +16,63 @@ let allCategories = [];
|
|
|
16
16
|
const selectedFileIds = new Set();
|
|
17
17
|
let lastCheckedIndex = -1;
|
|
18
18
|
let skillModalCurrent = null;
|
|
19
|
-
const allTemplates = [];
|
|
20
19
|
let searchResults = null;
|
|
21
20
|
|
|
21
|
+
// ---------- 视图模式(列表 / 卡片) ----------
|
|
22
|
+
const FILE_VIEW_KEY = 'jpage-file-view';
|
|
23
|
+
let viewMode = (() => { try { return localStorage.getItem(FILE_VIEW_KEY) === 'card' ? 'card' : 'list'; } catch { return 'list'; } })();
|
|
24
|
+
function setViewMode(mode) {
|
|
25
|
+
viewMode = mode;
|
|
26
|
+
try { localStorage.setItem(FILE_VIEW_KEY, mode); } catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 卡片缩略图懒加载管线(镜像 content-templates.js 的 thumbObserver:rootMargin 预加载 + 最多 3 并发)
|
|
30
|
+
// 注意:不能用 file-id Set 去重——翻页/筛选时 list.innerHTML='' 会销毁并重建卡片 DOM,
|
|
31
|
+
// 新卡片是全新元素,必须重新挂 iframe(会命中浏览器/渲染缓存,开销小)。
|
|
32
|
+
// 去重改为按「该卡片是否已有 iframe」判断,避免切回已访问过的页时缩略图卡在灰色占位。
|
|
33
|
+
let cardThumbObserver = null;
|
|
34
|
+
let cardThumbActive = 0;
|
|
35
|
+
const CARD_THUMB_MAX = 3;
|
|
36
|
+
const cardThumbQueue = [];
|
|
37
|
+
function ensureCardThumbObserver() {
|
|
38
|
+
if (cardThumbObserver) return cardThumbObserver;
|
|
39
|
+
cardThumbObserver = new IntersectionObserver(entries => {
|
|
40
|
+
entries.forEach(entry => {
|
|
41
|
+
if (!entry.isIntersecting) return;
|
|
42
|
+
const card = entry.target;
|
|
43
|
+
cardThumbObserver.unobserve(card);
|
|
44
|
+
enqueueCardThumb(card);
|
|
45
|
+
});
|
|
46
|
+
}, { rootMargin: '200px' });
|
|
47
|
+
return cardThumbObserver;
|
|
48
|
+
}
|
|
49
|
+
function enqueueCardThumb(card) {
|
|
50
|
+
if (cardThumbActive < CARD_THUMB_MAX) {
|
|
51
|
+
cardThumbActive++;
|
|
52
|
+
loadCardThumb(card).finally(() => {
|
|
53
|
+
cardThumbActive--;
|
|
54
|
+
if (cardThumbQueue.length) enqueueCardThumb(cardThumbQueue.shift());
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
cardThumbQueue.push(card);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// iframe.src 直接指向已有的 /api/files/:id/render —— HTML / Markdown / ZIP bundle 入口页均可渲染;
|
|
61
|
+
// 同源 iframe 默认携带 session cookie,用户自己的私有文件也能正常加载(loadFileWithPrivacy 放行 uploaded_by === userId)。
|
|
62
|
+
function loadCardThumb(card) {
|
|
63
|
+
const thumb = card.querySelector('.file-card-thumb');
|
|
64
|
+
// 已有 iframe(正在加载或已加载)则跳过,防止同一卡片重复挂载
|
|
65
|
+
if (!thumb || thumb.querySelector('.file-card-thumb-iframe')) return Promise.resolve();
|
|
66
|
+
thumb.innerHTML = '<div class="file-card-thumb-wrap"><iframe class="file-card-thumb-iframe" loading="lazy" title="预览"></iframe></div>';
|
|
67
|
+
const wrap = thumb.querySelector('.file-card-thumb-wrap');
|
|
68
|
+
const iframe = thumb.querySelector('.file-card-thumb-iframe');
|
|
69
|
+
return new Promise(resolve => {
|
|
70
|
+
iframe.addEventListener('load', () => { wrap.classList.add('loaded'); resolve(); }, { once: true });
|
|
71
|
+
iframe.addEventListener('error', () => resolve(), { once: true });
|
|
72
|
+
iframe.src = API_BASE + '/api/files/' + card.dataset.fileId + '/render';
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
22
76
|
// ---------- Home Page ----------
|
|
23
77
|
function renderHome(container) {
|
|
24
78
|
const tmpl = document.getElementById('home-template');
|
|
@@ -76,6 +130,7 @@ function renderHome(container) {
|
|
|
76
130
|
|
|
77
131
|
setupUpload(container);
|
|
78
132
|
setupFileFilter(container);
|
|
133
|
+
setupViewToggle(container);
|
|
79
134
|
loadTagsAndCategories(container);
|
|
80
135
|
loadFiles(container);
|
|
81
136
|
setupSkillModal();
|
|
@@ -154,6 +209,11 @@ function renderHome(container) {
|
|
|
154
209
|
settingsBtn.setAttribute('aria-expanded', 'false');
|
|
155
210
|
openMcpConfigModal();
|
|
156
211
|
});
|
|
212
|
+
settingsDropdown.querySelector('#menu-item-cli').addEventListener('click', () => {
|
|
213
|
+
settingsDropdown.classList.remove('open');
|
|
214
|
+
settingsBtn.setAttribute('aria-expanded', 'false');
|
|
215
|
+
openCliConfigModal();
|
|
216
|
+
});
|
|
157
217
|
settingsDropdown.querySelector('#menu-item-tokens')?.addEventListener('click', () => {
|
|
158
218
|
settingsDropdown.classList.remove('open');
|
|
159
219
|
settingsBtn.setAttribute('aria-expanded', 'false');
|
|
@@ -306,7 +366,7 @@ async function loadFiles(container, page) {
|
|
|
306
366
|
|
|
307
367
|
list.setAttribute('aria-busy', 'true');
|
|
308
368
|
list.classList.add('is-loading');
|
|
309
|
-
list.innerHTML = buildSkeletonCards(Math.min(pagination.limit, 5));
|
|
369
|
+
list.innerHTML = buildSkeletonCards(Math.min(pagination.limit, 5), viewMode);
|
|
310
370
|
empty.style.display = 'none';
|
|
311
371
|
countEl.textContent = '';
|
|
312
372
|
|
|
@@ -447,6 +507,11 @@ async function doBatchAction(container, action, data) {
|
|
|
447
507
|
}
|
|
448
508
|
|
|
449
509
|
function renderFileList(container, list, files) {
|
|
510
|
+
if (viewMode === 'card') {
|
|
511
|
+
renderCardList(container, list, files);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
list.classList.remove('view-card'); // 列表视图,恢复单列布局
|
|
450
515
|
const selectAllCb = container.querySelector('#select-all-checkbox');
|
|
451
516
|
if (selectAllCb) {
|
|
452
517
|
selectAllCb.checked = false;
|
|
@@ -504,7 +569,7 @@ function renderFileList(container, list, files) {
|
|
|
504
569
|
</div>
|
|
505
570
|
<div class="file-actions">
|
|
506
571
|
<button type="button" class="btn btn-small btn-star ${f.starred ? 'starred' : ''}" data-id="${f.id}">${f.starred ? '★' : '☆'}</button>
|
|
507
|
-
<button type="button" class="btn btn-small btn-copy-link" data-id="${f.id}"
|
|
572
|
+
<button type="button" class="btn btn-small btn-copy-link" data-id="${f.id}" title="复制链接" aria-label="复制链接"><svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
|
|
508
573
|
<div class="file-more-dropdown">
|
|
509
574
|
<button type="button" class="btn btn-small file-more-trigger" title="更多操作">⋯</button>
|
|
510
575
|
<div class="file-more-menu">
|
|
@@ -627,6 +692,75 @@ function renderFileList(container, list, files) {
|
|
|
627
692
|
});
|
|
628
693
|
}
|
|
629
694
|
|
|
695
|
+
// 卡片视图:每张卡片含一个实时 iframe 缩略图(懒加载)+ 文件名 + 标签 + 状态。
|
|
696
|
+
function renderCardList(container, list, files) {
|
|
697
|
+
// 切换容器为网格布局
|
|
698
|
+
list.classList.add('view-card');
|
|
699
|
+
// 卡片视图不支持全选(空间小),隐藏表头的全选 checkbox
|
|
700
|
+
const selectAllCb = container.querySelector('#select-all-checkbox');
|
|
701
|
+
if (selectAllCb) selectAllCb.checked = false;
|
|
702
|
+
const observer = ensureCardThumbObserver();
|
|
703
|
+
|
|
704
|
+
files.forEach((f) => {
|
|
705
|
+
const el = document.createElement('div');
|
|
706
|
+
el.className = 'file-card';
|
|
707
|
+
el.dataset.fileId = f.id;
|
|
708
|
+
if (selectedFileIds.has(f.id)) el.classList.add('selected');
|
|
709
|
+
const safeName = escapeHtml(f.original_name);
|
|
710
|
+
const isPublic = !!f.is_public;
|
|
711
|
+
const iconText = f.is_bundle ? 'ZIP' : (f.file_type === 'markdown' ? 'MD' : 'HTML');
|
|
712
|
+
const privacyBadge = isPublic
|
|
713
|
+
? '<span class="file-badge file-badge-public">公开</span>'
|
|
714
|
+
: '<span class="file-badge file-badge-private">私有</span>';
|
|
715
|
+
const versionBadge = f.version_count > 0
|
|
716
|
+
? `<span class="file-badge file-badge-version">v${f.version_count + 1}</span>` : '';
|
|
717
|
+
const tagBadges = (f.tags || []).slice(0, 3).map(t =>
|
|
718
|
+
`<span class="file-badge file-badge-tag" data-tag-id="${t.id}">${escapeHtml(t.name)}</span>`).join('');
|
|
719
|
+
const size = formatSize(f.size);
|
|
720
|
+
const timeStr = relativeTime(f.updated_at || f.created_at);
|
|
721
|
+
|
|
722
|
+
el.innerHTML = `
|
|
723
|
+
<div class="file-card-thumb" aria-hidden="true">
|
|
724
|
+
<div class="file-card-thumb-loading"></div>
|
|
725
|
+
</div>
|
|
726
|
+
<button type="button" class="file-card-icon-btn file-card-star ${f.starred ? 'starred' : ''}" data-id="${f.id}" aria-label="收藏" title="收藏">${f.starred ? '★' : '☆'}</button>
|
|
727
|
+
<button type="button" class="file-card-icon-btn file-card-copy" data-id="${f.id}" aria-label="复制链接" title="复制链接"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
|
|
728
|
+
<div class="file-card-name" title="${safeName}">${safeName}</div>
|
|
729
|
+
<div class="file-card-badges"><span class="file-badge file-badge-type">${iconText}</span>${privacyBadge}${versionBadge}${tagBadges}</div>
|
|
730
|
+
<div class="file-card-footer"><span>${size}</span><span>${timeStr}</span></div>
|
|
731
|
+
`;
|
|
732
|
+
el.setAttribute('role', 'button');
|
|
733
|
+
el.setAttribute('tabindex', '0');
|
|
734
|
+
el.setAttribute('aria-label', `打开 ${f.original_name}`);
|
|
735
|
+
|
|
736
|
+
// 整张卡片点击 → 预览(星标按钮单独拦截)
|
|
737
|
+
const openPreview = () => navigate('/view/' + f.id);
|
|
738
|
+
el.addEventListener('click', openPreview);
|
|
739
|
+
el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPreview(); } });
|
|
740
|
+
el.querySelector('.file-card-star').addEventListener('click', async e => {
|
|
741
|
+
e.stopPropagation();
|
|
742
|
+
await toggleStar(f.id, f.starred);
|
|
743
|
+
loadFiles(container);
|
|
744
|
+
});
|
|
745
|
+
el.querySelector('.file-card-copy').addEventListener('click', e => {
|
|
746
|
+
e.stopPropagation();
|
|
747
|
+
doCopyLink(f.share_key);
|
|
748
|
+
});
|
|
749
|
+
el.querySelectorAll('.file-badge-tag').forEach(badge => {
|
|
750
|
+
badge.addEventListener('click', e => {
|
|
751
|
+
e.stopPropagation();
|
|
752
|
+
filterState.tagId = parseInt(badge.dataset.tagId);
|
|
753
|
+
renderFilterDropdowns(container);
|
|
754
|
+
loadFiles(container, 1);
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// 注册懒加载:卡片滚到可视区附近才挂 iframe
|
|
759
|
+
observer.observe(el);
|
|
760
|
+
list.appendChild(el);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
630
764
|
function renderPagination(container) {
|
|
631
765
|
let wrap = container.querySelector('#pagination');
|
|
632
766
|
if (!wrap) {
|
|
@@ -694,6 +828,28 @@ function buildPageNumbers(current, total) {
|
|
|
694
828
|
return pages;
|
|
695
829
|
}
|
|
696
830
|
|
|
831
|
+
function setupViewToggle(container) {
|
|
832
|
+
const buttons = container.querySelectorAll('.view-toggle-btn');
|
|
833
|
+
const selectAllWrap = container.querySelector('.select-all-wrap'); // 卡片视图无单卡 checkbox,隐藏全选
|
|
834
|
+
const syncAllSelect = () => { if (selectAllWrap) selectAllWrap.hidden = (viewMode === 'card'); };
|
|
835
|
+
// 根据 viewMode 同步按钮的 active 态(首次进入 / 刷新后恢复持久化选择)
|
|
836
|
+
buttons.forEach(b => b.classList.toggle('active', b.dataset.view === viewMode));
|
|
837
|
+
syncAllSelect();
|
|
838
|
+
buttons.forEach(btn => {
|
|
839
|
+
btn.addEventListener('click', () => {
|
|
840
|
+
if (viewMode === btn.dataset.view) return;
|
|
841
|
+
// 切换前断开旧 observer,避免卡片 DOM 已销毁仍持有引用造成泄漏
|
|
842
|
+
if (cardThumbObserver) { cardThumbObserver.disconnect(); }
|
|
843
|
+
cardThumbQueue.length = 0;
|
|
844
|
+
cardThumbActive = 0;
|
|
845
|
+
setViewMode(btn.dataset.view);
|
|
846
|
+
buttons.forEach(b => b.classList.toggle('active', b === btn));
|
|
847
|
+
syncAllSelect();
|
|
848
|
+
applyFilters(container); // 复用现有重渲染流,重建列表/卡片
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
697
853
|
function setupFileFilter(container) {
|
|
698
854
|
const searchInput = container.querySelector('#search-input');
|
|
699
855
|
const searchClear = container.querySelector('#search-clear');
|
|
@@ -798,7 +954,7 @@ async function doRename(container, id, currentName) {
|
|
|
798
954
|
value: currentName,
|
|
799
955
|
validate: v => {
|
|
800
956
|
if (!v.trim()) return '文件名不能为空';
|
|
801
|
-
if (/[
|
|
957
|
+
if (/[/\\]/.test(v)) return '文件名不能包含 / 或 \\';
|
|
802
958
|
return null;
|
|
803
959
|
},
|
|
804
960
|
});
|
|
@@ -1006,7 +1162,8 @@ function openMcpConfigModal() {
|
|
|
1006
1162
|
<code class="mcp-value">${data.tokens.map(t => esc(t.token_prefix) + '…').join(', ')}</code>
|
|
1007
1163
|
</div>` : ''}
|
|
1008
1164
|
`;
|
|
1009
|
-
//
|
|
1165
|
+
// 多 MCP 客户端 Tab:共用同一份标准 JSON,差异仅在目标文件路径/说明文字。
|
|
1166
|
+
// CLI 不在此弹窗内——它走独立的「CLI 工具」菜单 + /api/cli/guide。
|
|
1010
1167
|
const configs = (data.configs && data.configs.length > 0)
|
|
1011
1168
|
? data.configs
|
|
1012
1169
|
: [{ id: 'generic', label: '通用', path: '', config: data.config }];
|
|
@@ -1031,6 +1188,7 @@ function openMcpConfigModal() {
|
|
|
1031
1188
|
const setConfig = (idx) => {
|
|
1032
1189
|
const c = configs[idx];
|
|
1033
1190
|
if (!c) return;
|
|
1191
|
+
// MCP 客户端 tab:显示 JSON
|
|
1034
1192
|
activeConfigJson = JSON.stringify(c.config, null, 2);
|
|
1035
1193
|
codeEl.textContent = activeConfigJson;
|
|
1036
1194
|
pathEl.textContent = c.path || '';
|
|
@@ -1044,12 +1202,16 @@ function openMcpConfigModal() {
|
|
|
1044
1202
|
});
|
|
1045
1203
|
const copyBtn = document.getElementById('mcp-copy-config');
|
|
1046
1204
|
if (copyBtn) {
|
|
1047
|
-
copyBtn.addEventListener('click', () => {
|
|
1048
|
-
navigator.clipboard
|
|
1205
|
+
copyBtn.addEventListener('click', async () => {
|
|
1206
|
+
// PR #9:navigator.clipboard 优先,不支持/失败时回退 execCommand(copyToClipboard)
|
|
1207
|
+
const ok = await copyToClipboard(activeConfigJson);
|
|
1208
|
+
if (ok) {
|
|
1049
1209
|
toast('已复制到剪贴板');
|
|
1050
1210
|
copyBtn.textContent = '已复制';
|
|
1051
1211
|
setTimeout(() => { copyBtn.textContent = '复制'; }, 2000);
|
|
1052
|
-
}
|
|
1212
|
+
} else {
|
|
1213
|
+
toast('复制失败', 'error');
|
|
1214
|
+
}
|
|
1053
1215
|
});
|
|
1054
1216
|
}
|
|
1055
1217
|
} else {
|
|
@@ -1079,6 +1241,54 @@ function closeMcpConfigModal() {
|
|
|
1079
1241
|
modal.setAttribute('aria-hidden', 'true');
|
|
1080
1242
|
}
|
|
1081
1243
|
|
|
1244
|
+
// ---------- CLI Config Modal ----------
|
|
1245
|
+
// CLI 与 MCP 是并列的两个客户端入口,各自独立弹窗。这里只渲染 CLI 用法文档。
|
|
1246
|
+
function openCliConfigModal() {
|
|
1247
|
+
const modal = document.getElementById('cli-config-modal');
|
|
1248
|
+
if (!modal) return;
|
|
1249
|
+
|
|
1250
|
+
if (!modal.dataset.bound) {
|
|
1251
|
+
modal.dataset.bound = '1';
|
|
1252
|
+
modal.querySelector('#cli-config-close').addEventListener('click', closeCliConfigModal);
|
|
1253
|
+
modal.querySelector('#cli-config-dismiss').addEventListener('click', closeCliConfigModal);
|
|
1254
|
+
modal.addEventListener('click', e => { if (e.target === modal) closeCliConfigModal(); });
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const detailEl = document.getElementById('cli-detail');
|
|
1258
|
+
detailEl.textContent = '加载中…';
|
|
1259
|
+
|
|
1260
|
+
api('/api/cli/guide').then(data => {
|
|
1261
|
+
// 渲染富文本用法文档;缓存纯文本供「复制文档」按钮使用
|
|
1262
|
+
detailEl.innerHTML = data.guideHtml || '<p>(无文档)</p>';
|
|
1263
|
+
const copyBtn = document.getElementById('cli-copy-guide');
|
|
1264
|
+
if (copyBtn) {
|
|
1265
|
+
copyBtn.onclick = async () => {
|
|
1266
|
+
const ok = await copyToClipboard(data.guideText || '');
|
|
1267
|
+
if (ok) {
|
|
1268
|
+
toast('已复制到剪贴板');
|
|
1269
|
+
copyBtn.textContent = '已复制';
|
|
1270
|
+
setTimeout(() => { copyBtn.textContent = '复制文档'; }, 2000);
|
|
1271
|
+
} else {
|
|
1272
|
+
toast('复制失败', 'error');
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
modal.hidden = false;
|
|
1277
|
+
modal.setAttribute('aria-hidden', 'false');
|
|
1278
|
+
}).catch(e => {
|
|
1279
|
+
detailEl.innerHTML = `<p style="color:var(--danger)">加载失败: ${escapeHtml(e.message)}</p>`;
|
|
1280
|
+
modal.hidden = false;
|
|
1281
|
+
modal.setAttribute('aria-hidden', 'false');
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function closeCliConfigModal() {
|
|
1286
|
+
const modal = document.getElementById('cli-config-modal');
|
|
1287
|
+
if (!modal) return;
|
|
1288
|
+
modal.hidden = true;
|
|
1289
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1082
1292
|
// ---------- Skills List Modal ----------
|
|
1083
1293
|
function openSkillsListModal() {
|
|
1084
1294
|
const modal = document.getElementById('skills-list-modal');
|
|
@@ -1218,7 +1428,6 @@ async function createUserDialog() {
|
|
|
1218
1428
|
|
|
1219
1429
|
// 需要挂到 window 上因为 users table 用了 inline onclick
|
|
1220
1430
|
async function editUserDialog(id, username, role, email) {
|
|
1221
|
-
const ops = ['修改用户名/邮箱', '修改角色', '重置密码'];
|
|
1222
1431
|
const choice = await dialogModal.confirm({
|
|
1223
1432
|
title: '编辑用户: ' + username,
|
|
1224
1433
|
message: '请选择操作',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { api } from '../api.js';
|
|
4
4
|
import { state, navigate } from '../app.js';
|
|
5
5
|
|
|
6
|
-
function renderLanding(container
|
|
6
|
+
function renderLanding(container) {
|
|
7
7
|
if (state.currentUser) { navigate('/'); return; }
|
|
8
8
|
const tmpl = document.getElementById('landing-template');
|
|
9
9
|
container.innerHTML = '';
|