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