feat(skills): add search functionality to Skills page

- Add search input with Search icon in SkillsPage component
- Implement useMemo-based filtering by skill name, description, and directory
- Display search results count when filtering is active
- Show "no results" message when no skills match the search query
- Add i18n translations for search UI (zh/en)
- Maintain responsive layout and consistent styling with existing UI
This commit is contained in:
YoVinchen
2025-11-23 00:10:07 +08:00
parent e7451bda22
commit be1c2ac76e
3 changed files with 73 additions and 14 deletions

View File

@@ -1,7 +1,8 @@
import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; import { useState, useEffect, useMemo, forwardRef, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react"; import { Input } from "@/components/ui/input";
import { RefreshCw, Search } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { SkillCard } from "./SkillCard"; import { SkillCard } from "./SkillCard";
import { RepoManagerPanel } from "./RepoManagerPanel"; import { RepoManagerPanel } from "./RepoManagerPanel";
@@ -24,6 +25,7 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
const [repos, setRepos] = useState<SkillRepo[]>([]); const [repos, setRepos] = useState<SkillRepo[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [repoManagerOpen, setRepoManagerOpen] = useState(false); const [repoManagerOpen, setRepoManagerOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const loadSkills = async (afterLoad?: (data: Skill[]) => void) => { const loadSkills = async (afterLoad?: (data: Skill[]) => void) => {
try { try {
@@ -162,6 +164,24 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
await Promise.all([loadRepos(), loadSkills()]); await Promise.all([loadRepos(), loadSkills()]);
}; };
// 过滤技能列表
const filteredSkills = useMemo(() => {
if (!searchQuery.trim()) return skills;
const query = searchQuery.toLowerCase();
return skills.filter((skill) => {
const name = skill.name?.toLowerCase() || "";
const description = skill.description?.toLowerCase() || "";
const directory = skill.directory?.toLowerCase() || "";
return (
name.includes(query) ||
description.includes(query) ||
directory.includes(query)
);
});
}, [skills, searchQuery]);
return ( return (
<div className="flex flex-col h-full min-h-0 bg-background/50"> <div className="flex flex-col h-full min-h-0 bg-background/50">
{/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */} {/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */}
@@ -189,9 +209,40 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
{t("skills.addRepo")} {t("skills.addRepo")}
</Button> </Button>
</div> </div>
) : (
<>
{/* 搜索框 */}
<div className="mb-6">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder={t("skills.searchPlaceholder")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
{searchQuery && (
<p className="mt-2 text-sm text-muted-foreground">
{t("skills.count", { count: filteredSkills.length })}
</p>
)}
</div>
{/* 技能列表或无结果提示 */}
{filteredSkills.length === 0 ? (
<div className="flex flex-col items-center justify-center h-48 text-center">
<p className="text-lg font-medium text-gray-900 dark:text-gray-100">
{t("skills.noResults")}
</p>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("skills.emptyDescription")}
</p>
</div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{skills.map((skill) => ( {filteredSkills.map((skill) => (
<SkillCard <SkillCard
key={skill.key} key={skill.key}
skill={skill} skill={skill}
@@ -201,6 +252,8 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
))} ))}
</div> </div>
)} )}
</>
)}
</div> </div>
</div> </div>

View File

@@ -735,7 +735,10 @@
"removeSuccess": "Repository {{owner}}/{{name}} removed", "removeSuccess": "Repository {{owner}}/{{name}} removed",
"removeFailed": "Failed to remove", "removeFailed": "Failed to remove",
"skillCount": "{{count}} skills detected" "skillCount": "{{count}} skills detected"
} },
"search": "Search Skills",
"searchPlaceholder": "Search skill name or description...",
"noResults": "No matching skills found"
}, },
"deeplink": { "deeplink": {
"confirmImport": "Confirm Import Provider", "confirmImport": "Confirm Import Provider",

View File

@@ -735,7 +735,10 @@
"removeSuccess": "仓库 {{owner}}/{{name}} 已删除", "removeSuccess": "仓库 {{owner}}/{{name}} 已删除",
"removeFailed": "删除失败", "removeFailed": "删除失败",
"skillCount": "识别到 {{count}} 个技能" "skillCount": "识别到 {{count}} 个技能"
} },
"search": "搜索技能",
"searchPlaceholder": "搜索技能名称或描述...",
"noResults": "未找到匹配的技能"
}, },
"deeplink": { "deeplink": {
"confirmImport": "确认导入供应商配置", "confirmImport": "确认导入供应商配置",