import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { SkillCard } from "./SkillCard"; import { RepoManagerPanel } from "./RepoManagerPanel"; import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills"; import { formatSkillError } from "@/lib/errors/skillErrorParser"; interface SkillsPageProps { onClose?: () => void; } export interface SkillsPageHandle { refresh: () => void; openRepoManager: () => void; } export const SkillsPage = forwardRef( ({ onClose: _onClose }, ref) => { const { t } = useTranslation(); const [skills, setSkills] = useState([]); const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(true); const [repoManagerOpen, setRepoManagerOpen] = useState(false); const loadSkills = async (afterLoad?: (data: Skill[]) => void) => { try { setLoading(true); const data = await skillsApi.getAll(); setSkills(data); if (afterLoad) { afterLoad(data); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // 传入 "skills.loadFailed" 作为标题 const { title, description } = formatSkillError( errorMessage, t, "skills.loadFailed", ); toast.error(title, { description, duration: 8000, }); console.error("Load skills failed:", error); } finally { setLoading(false); } }; const loadRepos = async () => { try { const data = await skillsApi.getRepos(); setRepos(data); } catch (error) { console.error("Failed to load repos:", error); } }; useEffect(() => { Promise.all([loadSkills(), loadRepos()]); }, []); useImperativeHandle(ref, () => ({ refresh: () => loadSkills(), openRepoManager: () => setRepoManagerOpen(true), })); const handleInstall = async (directory: string) => { try { await skillsApi.install(directory); toast.success(t("skills.installSuccess", { name: directory })); await loadSkills(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // 使用错误解析器格式化错误,传入 "skills.installFailed" const { title, description } = formatSkillError( errorMessage, t, "skills.installFailed", ); toast.error(title, { description, duration: 10000, // 延长显示时间让用户看清 }); console.error("Install skill failed:", { directory, error, message: errorMessage, }); } }; const handleUninstall = async (directory: string) => { try { await skillsApi.uninstall(directory); toast.success(t("skills.uninstallSuccess", { name: directory })); await loadSkills(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // 使用错误解析器格式化错误,传入 "skills.uninstallFailed" const { title, description } = formatSkillError( errorMessage, t, "skills.uninstallFailed", ); toast.error(title, { description, duration: 10000, }); console.error("Uninstall skill failed:", { directory, error, message: errorMessage, }); } }; const handleAddRepo = async (repo: SkillRepo) => { await skillsApi.addRepo(repo); let repoSkillCount = 0; await Promise.all([ loadRepos(), loadSkills((data) => { repoSkillCount = data.filter( (skill) => skill.repoOwner === repo.owner && skill.repoName === repo.name && (skill.repoBranch || "main") === (repo.branch || "main"), ).length; }), ]); toast.success( t("skills.repo.addSuccess", { owner: repo.owner, name: repo.name, count: repoSkillCount, }), ); }; const handleRemoveRepo = async (owner: string, name: string) => { await skillsApi.removeRepo(owner, name); toast.success(t("skills.repo.removeSuccess", { owner, name })); await Promise.all([loadRepos(), loadSkills()]); }; return (
{/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */} {/* 技能网格(可滚动详情区域) */}
{loading ? (
) : skills.length === 0 ? (

{t("skills.empty")}

{t("skills.emptyDescription")}

) : (
{skills.map((skill) => ( ))}
)}
{/* 仓库管理面板 */} {repoManagerOpen && ( setRepoManagerOpen(false)} /> )}
); }, ); SkillsPage.displayName = "SkillsPage";