fix(skills): keep SkillHub context after installation

This commit is contained in:
techflag
2026-09-26 17:35:10 +08:00
parent 70f17290bc
commit 303bf3773d
3 changed files with 14 additions and 7 deletions
@@ -34,13 +34,13 @@ async function skillHub<T extends { ok: boolean; error?: string }>(method: strin
}
/** Reuse the installed DSH plugin's search and verified installation path. */
export function SkillHubPanel({ query, onInstalled }: { query: string; onInstalled: () => Promise<void> }) {
export function SkillHubPanel({ query, onInstalled, onOpenInstalled }: { query: string; onInstalled: () => Promise<void>; onOpenInstalled: () => void }) {
const [cards, setCards] = useState<SkillHubCard[]>([]);
const [total, setTotal] = useState(0);
const [busy, setBusy] = useState(true);
const [error, setError] = useState('');
const [installing, setInstalling] = useState('');
const [revision, setRevision] = useState(0);
const [installedName, setInstalledName] = useState('');
const [page, setPage] = useState(0);
useEffect(() => { setPage(0); }, [query]);
@@ -55,14 +55,15 @@ export function SkillHubPanel({ query, onInstalled }: { query: string; onInstall
.finally(() => { if (!controller.signal.aborted) setBusy(false); });
}, 250);
return () => { window.clearTimeout(timer); controller.abort(); };
}, [query, page, revision]);
}, [query, page]);
const install = async (card: SkillHubCard) => {
setInstalling(card.slug); setError('');
try {
await skillHub('install', { slug: card.slug });
await onInstalled();
setRevision(value => value + 1);
setCards(current => current.map(item => item.slug === card.slug ? { ...item, installed: true } : item));
setInstalledName(card.name);
void onInstalled();
} catch (cause) { setError(cause instanceof Error ? cause.message : '安装失败,请重试。'); }
finally { setInstalling(''); }
};
@@ -73,11 +74,12 @@ export function SkillHubPanel({ query, onInstalled }: { query: string; onInstall
return <section className="skillhub-market" aria-label="SkillHub 技能目录">
<div className="market-head"><h2>SkillHub <span className="market-count">{total}</span></h2><span className="muted">来源:SkillHub · 安装到本机 DSH 技能目录</span></div>
{installedName && <div className="skillhub-success" role="status"><span>已安装「{installedName}」。</span><Button onClick={onOpenInstalled}>查看已安装</Button><button className="skillhub-dismiss" aria-label="关闭安装提示" onClick={() => setInstalledName('')}>×</button></div>}
{error && <p className="catalog-note" role="alert">{error}</p>}
{busy ? <p className="muted" role="status">正在读取 SkillHub…</p> : cards.length ? <div className="grid">{cards.map(card =>
<article className="card market-card" key={card.slug}>
<div className="card-top"><a className="card-open" href={card.pageUrl?.startsWith('https://skillhub.cn/skills/') ? card.pageUrl : `https://skillhub.cn/skills/${encodeURIComponent(card.slug)}`} target="_blank" rel="noopener noreferrer" aria-label={`查看 ${card.name} 的来源页`}><SkillHubIcon card={card} /><span className="card-title"><strong title={card.name}>{card.name}</strong><small>{card.categoryLabel || card.owner || card.slug}</small></span></a>
<Button className="install" disabled={card.installed || Boolean(installing)} aria-label={card.installed ? `已安装 ${card.name}` : `安装 ${card.name}`} onClick={() => void install(card)}>{card.installed ? '✓' : installing === card.slug ? '…' : '+'}</Button></div>
<Button className={`install${card.installed || installing === card.slug ? ' is-status' : ''}${card.installed ? ' is-installed' : ''}`} disabled={card.installed || Boolean(installing)} aria-label={card.installed ? `已安装 ${card.name}` : `安装 ${card.name}`} onClick={() => void install(card)}>{card.installed ? '已安装' : installing === card.slug ? '安装中…' : '+'}</Button></div>
<p className="muted">{card.description}</p>
<small className="skillhub-meta">版本 {card.version || '未标注'} · 许可证请查看来源页</small>
</article>)}</div> : !error && <p className="muted">没有找到匹配的技能。</p>}
@@ -264,7 +264,7 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star
</header>
<div className="section-head"><h1>技能市场</h1><Button className="refresh" onClick={() => void refresh()} disabled={busy}>刷新</Button></div>
<nav className="skill-source-tabs" aria-label="技能来源"><button className={marketSource === 'local' ? 'active' : ''} aria-current={marketSource === 'local' ? 'page' : undefined} onClick={() => setMarketSource('local')}>本地技能</button><button className={marketSource === 'skillhub' ? 'active' : ''} aria-current={marketSource === 'skillhub' ? 'page' : undefined} onClick={() => setMarketSource('skillhub')}>SkillHub</button></nav>
{marketSource === 'skillhub' ? <SkillHubPanel query={query} onInstalled={refresh} /> : <>
{marketSource === 'skillhub' ? <SkillHubPanel query={query} onInstalled={refresh} onOpenInstalled={openInstalled} /> : <>
<nav className="category-tabs" aria-label="技能分类"><button className={selectedCategory === ALL ? 'active' : ''} aria-current={selectedCategory === ALL ? 'page' : undefined} onClick={() => setCategory(ALL)}>全部</button>{categories.map(label => <button key={label} className={selectedCategory === label ? 'active' : ''} aria-current={selectedCategory === label ? 'page' : undefined} onClick={() => setCategory(current => current === label ? ALL : label)}>{label}</button>)}</nav>
{catalog?.status === 'invalid' && <p className="catalog-note" role="note">技能目录暂时不可用,已安装的技能仍可使用。</p>}
{batchMode && batchBar}
@@ -115,6 +115,11 @@ export const skillsMarketCss = `${modalCss}
.wd-skills .skillhub-icon img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:var(--dsw-alias-bg-layer-2)}
.wd-skills .skillhub-icon img[hidden]{display:none}
.wd-skills .skillhub-meta{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px}
.wd-skills .skillhub-market .install.is-status{width:auto;min-width:58px;padding:0 8px;border-radius:8px;font-size:12px}
.wd-skills .skillhub-market .install.is-installed:disabled{color:var(--dsw-alias-state-success-primary);opacity:1}
.wd-skills .skillhub-success{display:flex;align-items:center;gap:12px;margin:0 0 18px;padding:10px 14px;border:1px solid var(--dsw-alias-state-success-primary);border-radius:10px;background:var(--dsw-alias-bg-layer-2);font-size:13px}
.wd-skills .skillhub-success span{flex:1}
.wd-skills .skillhub-success .skillhub-dismiss{border:0;background:transparent;font-size:18px}
.wd-skills .skillhub-pagination{display:flex;justify-content:center;align-items:center;gap:8px;flex-wrap:wrap;margin:28px 0 12px}
.wd-skills .skillhub-pagination button{min-width:36px;background:transparent}
.wd-skills .skillhub-pagination button.active{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);font-weight:600}