diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx index 1f224cd2..a54d8323 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -1,10 +1,11 @@ "use client"; -import { - AlertCircle, - ArchiveIcon, - ArrowDownUpIcon, - CheckCircle2, +import { + AlertCircle, + ArchiveIcon, + ArrowDownUpIcon, + BookmarkIcon, + CheckCircle2, ChevronLeftIcon, ChevronRightIcon, ClockIcon, @@ -12,22 +13,22 @@ import { FileArchive, GlobeIcon, InfoIcon, - Loader2, - LockIcon, - PinIcon, - RefreshCwIcon, + Loader2, + LockIcon, + PinIcon, + RefreshCwIcon, SparklesIcon, TagsIcon, Trash2Icon, TrendingUpIcon, Upload, -} from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +} from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -37,8 +38,8 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { - Empty, - EmptyDescription, + Empty, + EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, @@ -51,9 +52,9 @@ import { ItemContent, ItemDescription, } from "@/components/ui/item"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog"; import { TagChipList } from "@/components/workspace/tags/tag-chip"; import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog"; @@ -67,6 +68,7 @@ import { useDuplicateSkill, useInstallSkillUpload, useReconcileCustomSkills, + useSetSkillFeatured, useSetSkillPinned, useSetSkillPublished, useSkills, @@ -92,16 +94,16 @@ export function SkillSettingsPage({ onClose }: { onClose?: () => void } = {}) { ); } -const PAGE_SIZE = 10; -type SkillSort = - | "time_desc" - | "time_asc" - | "activity_desc" - | "activity_asc" - | "usage_desc" - | "usage_asc" - | "name_asc" - | "name_desc"; +const PAGE_SIZE = 10; +type SkillSort = + | "time_desc" + | "time_asc" + | "activity_desc" + | "activity_asc" + | "usage_desc" + | "usage_asc" + | "name_asc" + | "name_desc"; /** 重新扫描成功 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */ const rescanSuccessToastDarkOptions = { @@ -136,21 +138,21 @@ const rescanErrorToastDarkOptions = { icon: , } as const; -function SkillSettingsList({ onClose }: { onClose?: () => void }) { - const { t } = useI18n(); - const { theme } = useTheme(); - const router = useRouter(); - const [filter, setFilter] = useState("mine"); +function SkillSettingsList({ onClose }: { onClose?: () => void }) { + const { t } = useI18n(); + const { theme } = useTheme(); + const router = useRouter(); + const [filter, setFilter] = useState("mine"); const [installOpen, setInstallOpen] = useState(false); const [staleSheetOpen, setStaleSheetOpen] = useState(false); const [archiveSheetOpen, setArchiveSheetOpen] = useState(false); - const [search, setSearch] = useState(""); - const [tagIds, setTagIds] = useState([]); - const [sort, setSort] = useState("time_desc"); - const [page, setPage] = useState(1); - const reconcileMutation = useReconcileCustomSkills(); - // Name + tag filtering is resolved server-side via the /api/skills query. - const { skills, isLoading, error } = useSkills({ search, tagIds, sort }); + const [search, setSearch] = useState(""); + const [tagIds, setTagIds] = useState([]); + const [sort, setSort] = useState("time_desc"); + const [page, setPage] = useState(1); + const reconcileMutation = useReconcileCustomSkills(); + // Name + tag filtering is resolved server-side via the /api/skills query. + const { skills, isLoading, error } = useSkills({ search, tagIds, sort }); const storedAuth = useMemo(() => getStoredAuth(), []); const currentUserId = storedAuth?.user_id ?? null; @@ -195,36 +197,38 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { }; }, [skills, currentUserId]); - const filteredSkills = filter === "mine" ? activeMine : activeAll; - const orderedSkills = useMemo( - () => - filteredSkills - .map((skill, index) => ({ skill, index })) - .sort((a, b) => { - const pinnedDelta = Number(!!b.skill.pinned) - Number(!!a.skill.pinned); - return pinnedDelta || a.index - b.index; - }) - .map(({ skill }) => skill), - [filteredSkills], - ); - const staleCount = staleVisible.length; - const archivedCount = archivedVisible.length; - - const totalPages = Math.max(1, Math.ceil(orderedSkills.length / PAGE_SIZE)); + const filteredSkills = filter === "mine" ? activeMine : activeAll; + const orderedSkills = useMemo( + () => + filteredSkills + .map((skill, index) => ({ skill, index })) + .sort((a, b) => { + // 置顶(featured)决定排序;钉选(pinned)只管防归档,不影响顺序。 + const featuredDelta = + Number(!!b.skill.featured) - Number(!!a.skill.featured); + return featuredDelta || a.index - b.index; + }) + .map(({ skill }) => skill), + [filteredSkills], + ); + const staleCount = staleVisible.length; + const archivedCount = archivedVisible.length; + + const totalPages = Math.max(1, Math.ceil(orderedSkills.length / PAGE_SIZE)); // Filter / search / tag changes may shrink the result set — pull the page // back into range without forcing a reset on every keystroke. - useEffect(() => { - if (page > totalPages) setPage(totalPages); - }, [page, totalPages]); - useEffect(() => { - setPage(1); - }, [search, tagIds, sort]); - const pagedSkills = useMemo( - () => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), - [orderedSkills, page], - ); + useEffect(() => { + if (page > totalPages) setPage(totalPages); + }, [page, totalPages]); + useEffect(() => { + setPage(1); + }, [search, tagIds, sort]); + const pagedSkills = useMemo( + () => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), + [orderedSkills, page], + ); - const handleRescan = async () => { + const handleRescan = async () => { try { const result = await reconcileMutation.mutateAsync(); if (result.adopted.length > 0) { @@ -323,54 +327,54 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { {t.settings.skills.installSkill} - - - setSort(v as SkillSort)}> - - - - - - 最新创建 - 最早创建 - 最近使用 - 最早使用 - 使用最多 - 使用最少 - 名称 A-Z - 名称 Z-A - - - } - /> + + + setSort(v as SkillSort)}> + + + + + + 最新创建 + 最早创建 + 最近使用 + 最早使用 + 使用最多 + 使用最少 + 名称 A-Z + 名称 Z-A + + + } + /> {isLoading && (
{t.common.loading}
)} {error &&
Error: {error.message}
} - {!isLoading && !error && filteredSkills.length === 0 && ( - - )} - {!isLoading && !error && filteredSkills.length > 0 && ( -
- {pagedSkills.map((skill) => ( - - ))} -
- )} + {!isLoading && !error && filteredSkills.length === 0 && ( + + )} + {!isLoading && !error && filteredSkills.length > 0 && ( +
+ {pagedSkills.map((skill) => ( + + ))} +
+ )} {!isLoading && !error && totalPages > 1 && (
+ )} + {canManage && ( + - )} + : t.settings.skills.pinAction + } + > + + + )} {canTogglePublish && (