diff --git a/web/static/dev.html b/web/static/dev.html index 48983e1..7e9b6ee 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -205,12 +205,16 @@ flex-shrink: 0; border-top: 1px solid var(--border); padding: 8px; display: flex; gap: 6px; } - #rail-resources > button { flex: 1; font-size: 13px; } + #rail-resources > button { + flex: 1; font-size: 13px; + display: inline-flex; align-items: center; justify-content: center; gap: 6px; + } + #rail-resources > button svg { flex-shrink: 0; opacity: .85; } - /* ───── 技能查看 modal ───── */ + /* ───── 技能查看 modal(两栏 master-detail)───── */ #skills-modal { z-index: 112; } #skills-modal .card { - width: 720px; max-width: 92vw; max-height: 84vh; + width: 880px; max-width: 94vw; height: 80vh; max-height: 80vh; display: flex; flex-direction: column; } #skills-modal h3 { @@ -219,21 +223,28 @@ display: flex; align-items: center; gap: 8px; } #skills-modal h3 .spacer { flex: 1; } + #skills-modal h3 svg { opacity: .85; } #skills-modal .sk-x { border: none; background: transparent; font-size: 16px; cursor: pointer; color: var(--muted); padding: 2px 6px; } - #skills-modal #sk-back { margin-right: 4px; } - #skills-modal .sk-detail-name { font-weight: 600; } - #skills-modal .body { padding: 12px 16px; overflow: auto; } + /* 两栏:左列表 / 右正文 */ + #sk-cols { flex: 1; display: flex; min-height: 0; } + #sk-list { + width: 290px; flex-shrink: 0; overflow: auto; + padding: 12px; border-right: 1px solid var(--border); + } + #sk-detail { flex: 1; min-width: 0; overflow: auto; padding: 16px 20px; } + .sk-empty { color: var(--muted); font-size: 13px; padding: 24px 8px; text-align: center; } + .sk-group-title { font-weight: 600; font-size: 12px; color: var(--muted); margin: 0 0 8px; } + .sk-group-title.mt { margin-top: 16px; } .sk-item { - display: flex; align-items: center; gap: 10px; - padding: 8px 10px; border: 1px solid var(--border); + padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--r-md); margin-bottom: 6px; cursor: pointer; } .sk-item:hover { border-color: var(--accent); background: #fafafa; } - .sk-item-main { flex: 1; min-width: 0; } + .sk-item.active { border-color: var(--accent); background: rgba(120,120,200,0.07); } .sk-item .sk-name { font-weight: 600; font-size: 13px; } .sk-item .sk-desc { font-size: 12px; color: var(--muted); margin-top: 2px; @@ -242,23 +253,36 @@ .sk-badge { font-size: 10px; font-weight: 500; color: var(--accent); border: 1px solid var(--accent); border-radius: 8px; padding: 0 5px; - margin-left: 4px; white-space: nowrap; + white-space: nowrap; } - .sk-del { flex-shrink: 0; } + .sk-name .sk-badge { margin-left: 4px; } .sk-loaderr { margin-top: 14px; padding: 8px 10px; font-size: 12px; border: 1px solid var(--accent); border-radius: var(--r-md); color: var(--accent); background: rgba(220,80,80,0.05); } - .sk-detail { font-size: 13px; line-height: 1.6; } - .sk-detail pre { + /* 右栏正文头 + markdown */ + .sk-d-head { + display: flex; align-items: center; gap: 8px; margin-bottom: 12px; + padding-bottom: 10px; border-bottom: 1px solid var(--border); + } + .sk-d-head .sk-d-name { font-weight: 600; font-size: 15px; } + .sk-d-head .spacer { flex: 1; } + .sk-detail-md { font-size: 13px; line-height: 1.6; } + .sk-detail-md pre { white-space: pre-wrap; word-break: break-word; background: #f5f5f5; padding: 10px; border-radius: var(--r-md); overflow: auto; } - .sk-detail code { word-break: break-word; } - .sk-detail h1, .sk-detail h2, .sk-detail h3 { margin: 14px 0 6px; } - .sk-detail table { border-collapse: collapse; } - .sk-detail th, .sk-detail td { border: 1px solid var(--border); padding: 4px 8px; } + .sk-detail-md code { word-break: break-word; } + .sk-detail-md h1, .sk-detail-md h2, .sk-detail-md h3 { margin: 14px 0 6px; } + .sk-detail-md table { border-collapse: collapse; } + .sk-detail-md th, .sk-detail-md td { border: 1px solid var(--border); padding: 4px 8px; } + /* 窄屏:两栏改上下堆叠 */ + @media (max-width: 640px) { + #skills-modal .card { height: 88vh; max-height: 88vh; } + #sk-cols { flex-direction: column; } + #sk-list { width: auto; max-height: 38vh; border-right: none; border-bottom: 1px solid var(--border); } + } /* ───── 3-pane layout ───── */ #app { display: none; height: 100vh; } @@ -1054,11 +1078,15 @@ @@ -1129,7 +1157,10 @@
- +
diff --git a/web/static/js/skills.js b/web/static/js/skills.js index 11766d0..422f143 100644 --- a/web/static/js/skills.js +++ b/web/static/js/skills.js @@ -1,5 +1,6 @@ -// 技能 modal:查看「平台 skill」/「我的 skill」两组列表,点任一项展开完整 SKILL.md, -// 「我的」每项可删除(平台 skill 只读)。左侧 rail 底部「技能」按钮触发。 +// 技能 modal:两栏 master-detail。左栏「平台 skill / 我的 skill」两组列表, +// 右栏展示选中 skill 的完整 SKILL.md;删除按钮在右栏正文头部(只对「我的」)。 +// 左侧 rail 底部「技能」按钮触发。 // 后端:GET /v1/skills(列表)、GET /v1/skills/{name}(正文)、DELETE /v1/skills/{name}(只删 user 源)。 // 创建 / 改 / fork 仍走对话(save_skill / fork_skill / skill-creator)。 import { $ } from "./dom.js"; @@ -8,8 +9,11 @@ import { api } from "./api.js"; import { escapeHtml } from "./format.js"; import { renderMd, highlightIn } from "./markdown.js"; +const PLACEHOLDER = '
← 从左侧选一个 skill 查看完整说明
'; + function openSkillsModal() { $("skills-modal").classList.add("show"); + $("sk-detail").innerHTML = PLACEHOLDER; renderList(); } export function closeSkillsModal() { @@ -20,27 +24,20 @@ function itemHtml(s) { const badge = s.overrides_builtin ? ' 已覆盖平台同名' : ""; - const del = - s.source === "user" - ? `` - : ""; return `
-
-
${escapeHtml(s.name)}${badge}
-
${escapeHtml(s.description || "")}
-
${del} +
${escapeHtml(s.name)}${badge}
+
${escapeHtml(s.description || "")}
`; } async function renderList() { - const body = $("sk-body"); - $("sk-title").textContent = "技能"; - body.innerHTML = '
加载中…
'; + const list = $("sk-list"); + list.innerHTML = '
加载中…
'; let data; try { data = await api("GET", "/v1/skills"); } catch (e) { - body.innerHTML = `
加载失败: ${escapeHtml(e.message)}
`; + list.innerHTML = `
加载失败: ${escapeHtml(e.message)}
`; return; } state.skills = data.skills || []; // 顺手刷新新建任务下拉的缓存 @@ -52,10 +49,10 @@ async function renderList() { html += platform.map(itemHtml).join("") || '
(无)
'; - html += `
我的 skill (${mine.length})
`; + html += `
我的 skill (${mine.length})
`; html += mine.length ? mine.map(itemHtml).join("") - : '
还没有。让助手「帮我做个 skill」或「把某个平台 skill fork 成我的」即可创建。
'; + : '
还没有。让助手「帮我做个 skill」或「把某个平台 skill fork 成我的」即可创建。
'; if (data.load_errors && data.load_errors.length) { const errs = data.load_errors @@ -63,23 +60,34 @@ async function renderList() { .join(";"); html += `
⚠ ${data.load_errors.length} 个 skill 因格式问题未加载:${errs}
`; } - body.innerHTML = html; + list.innerHTML = html; } -async function showDetail(name) { - const body = $("sk-body"); - $("sk-title").innerHTML = `${escapeHtml(name)}`; - $("sk-back").onclick = renderList; - body.innerHTML = '
加载中…
'; +async function selectSkill(name, itemEl) { + // 左栏高亮 + $("sk-list").querySelectorAll(".sk-item.active").forEach((el) => el.classList.remove("active")); + if (itemEl) itemEl.classList.add("active"); + + const detail = $("sk-detail"); + detail.innerHTML = '
加载中…
'; let data; try { data = await api("GET", "/v1/skills/" + encodeURIComponent(name)); } catch (e) { - body.innerHTML = `
加载失败: ${escapeHtml(e.message)}
`; + detail.innerHTML = `
加载失败: ${escapeHtml(e.message)}
`; return; } - body.innerHTML = `
${renderMd(data.content)}
`; - highlightIn(body); + const isUser = data.source === "user"; + const srcBadge = isUser + ? `${data.overrides_builtin ? "我的 · 已覆盖平台" : "我的"}` + : `平台`; + const delBtn = isUser + ? `` + : ""; + detail.innerHTML = + `
${escapeHtml(data.name)}${srcBadge}${delBtn}
` + + `
${renderMd(data.content)}
`; + highlightIn(detail); } // ───── 顶层绑定 ───── @@ -89,24 +97,26 @@ $("skills-modal").addEventListener("click", (e) => { if (e.target.id === "skills-modal") closeSkillsModal(); // 点遮罩关闭 }); -// 列表区事件委托:删除(冒泡到 [data-del])优先于点开详情(.sk-item) -$("sk-body").addEventListener("click", async (e) => { - const del = e.target.closest("[data-del]"); - if (del) { - e.stopPropagation(); - const name = del.getAttribute("data-del"); - if (!confirm(`删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`)) return; - del.disabled = true; - try { - await api("DELETE", "/v1/skills/" + encodeURIComponent(name)); - state.skills = null; // 失效缓存,新建任务下拉下次重拉 - renderList(); - } catch (err) { - alert("删除失败: " + err.message); - del.disabled = false; - } - return; - } +// 左栏:点 item 选中 → 右栏载正文 +$("sk-list").addEventListener("click", (e) => { const item = e.target.closest(".sk-item"); - if (item) showDetail(item.getAttribute("data-name")); + if (item) selectSkill(item.getAttribute("data-name"), item); +}); + +// 右栏:删除(冒泡到 [data-del]) +$("sk-detail").addEventListener("click", async (e) => { + const del = e.target.closest("[data-del]"); + if (!del) return; + const name = del.getAttribute("data-del"); + if (!confirm(`删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`)) return; + del.disabled = true; + try { + await api("DELETE", "/v1/skills/" + encodeURIComponent(name)); + state.skills = null; // 失效缓存,新建任务下拉下次重拉 + $("sk-detail").innerHTML = PLACEHOLDER; + renderList(); + } catch (err) { + alert("删除失败: " + err.message); + del.disabled = false; + } });