feat(skills): 技能管理 - 支持后台维护一级分类 (#694)
Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { Check, LoaderCircle, Plus, Search, Sparkles, Users } from "lucide-react
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import type { CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { Skill, SkillCategory, SkillLibraryCategory } from "@/services/api/skills";
|
||||
import { SkillCurationBrowser, matchesCuration, type useSkillCuration } from "@/components/skills/skill-curation-browser";
|
||||
import { SkillCurationBrowser, matchesCuration, effectiveCurationRoot, type useSkillCuration } from "@/components/skills/skill-curation-browser";
|
||||
|
||||
type SkillLibraryTab = "enabled" | "installed" | "market";
|
||||
|
||||
@@ -135,6 +135,7 @@ export function CanvasAgentSkillLibraryModal({
|
||||
setTab(value);
|
||||
setLibraryCategoryId("all");
|
||||
onCategoryChange("all");
|
||||
onPlatformCategoryChange("");
|
||||
};
|
||||
|
||||
const selectMarketplaceCategory = (value: string) => {
|
||||
@@ -206,7 +207,7 @@ export function CanvasAgentSkillLibraryModal({
|
||||
<div className="canvas-agent-skill-library-category-group">
|
||||
<div className="canvas-agent-skill-library-category-heading">技能广场分类</div>
|
||||
<SkillTab active={tab === "market" && category === "all"} label="全部技能" count={marketplaceCategoryTotal} onClick={() => changeTab("market")} />
|
||||
{categoryItems.map((item) => (
|
||||
{!curationState.curation?.enabled && categoryItems.map((item) => (
|
||||
<SkillTab
|
||||
key={item.value}
|
||||
active={tab === "market" && category === item.value}
|
||||
@@ -232,6 +233,7 @@ export function CanvasAgentSkillLibraryModal({
|
||||
<SkillLibraryCard
|
||||
key={skill.skillId}
|
||||
skill={skill}
|
||||
rootLabel={curationState.curation?.enabled ? curationState.curation.roots?.find((root) => root.id === effectiveCurationRoot(skill, curationState.curation!))?.name : undefined}
|
||||
theme={theme}
|
||||
categories={categories}
|
||||
selected={selectedSkillIds.includes(skill.skillId)}
|
||||
@@ -277,8 +279,9 @@ function SkillTab({ active, label, count, onClick }: { active: boolean; label: s
|
||||
);
|
||||
}
|
||||
|
||||
function SkillLibraryCard({ skill, theme, categories, selected, canSelect, onToggle, onInstall }: {
|
||||
function SkillLibraryCard({ skill, rootLabel, theme, categories, selected, canSelect, onToggle, onInstall }: {
|
||||
skill: Skill;
|
||||
rootLabel?: string;
|
||||
theme: CanvasTheme;
|
||||
categories: SkillCategory[];
|
||||
selected: boolean;
|
||||
@@ -292,7 +295,7 @@ function SkillLibraryCard({ skill, theme, categories, selected, canSelect, onTog
|
||||
|| skill.showcaseMedia?.find((item) => item.showcaseUrl);
|
||||
const coverUrl = cover?.showcaseUrl;
|
||||
const hasCover = Boolean(coverUrl && !coverFailed);
|
||||
const categoryLabel = categories.find((item) => item.value === skill.tag)?.label || "其他";
|
||||
const categoryLabel = rootLabel || categories.find((item) => item.value === skill.tag)?.label || "其他";
|
||||
const author = skill.effectiveUser?.name || "影策创作者";
|
||||
const addedCount = formatSkillCount(skill.addedCount || 0);
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, se
|
||||
const skillBrowseKey = JSON.stringify([userId, skillTag, debouncedSkillSearch, platformCategory, curationState.curation?.revision, curationState.curation?.enabled]);
|
||||
const skillBrowseKeyRef = useRef(skillBrowseKey);
|
||||
skillBrowseKeyRef.current = skillBrowseKey;
|
||||
useEffect(() => { if (curationState.curation?.enabled) setSkillTag("all"); }, [curationState.curation?.enabled]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [approvalSubmitting, setApprovalSubmitting] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
@@ -3,10 +3,24 @@ import { Alert, Button, Select, Space } from "antd";
|
||||
import { getActiveUserScope } from "@/lib/user-scope";
|
||||
import { getSkillCuration, type SkillCuration } from "@/services/api/skill-curation";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import { fallbackSkillCategories } from "@/pages/skills/skill-catalog";
|
||||
import { Film, ShoppingBag, Megaphone, Landmark, Palette, Users, Wrench, Shapes } from "lucide-react";
|
||||
|
||||
export const curationIconKeys = ["shapes", "film", "shopping-bag", "megaphone", "landmark", "palette", "users", "wrench"];
|
||||
export const curationIconLabels: Record<string, string> = { shapes: "默认", film: "影视", "shopping-bag": "购物袋", megaphone: "喇叭", landmark: "地标", palette: "调色盘", users: "社群", wrench: "工具" };
|
||||
export function curationIcon(key?: string) {
|
||||
return ({ film: Film, "shopping-bag": ShoppingBag, megaphone: Megaphone, landmark: Landmark, palette: Palette, users: Users, wrench: Wrench } as Record<string, typeof Shapes>)[key || ""] || Shapes;
|
||||
}
|
||||
export function effectiveCurationRoot(skill: Skill, data: SkillCuration) {
|
||||
const id = (!skill.isPrivate && data.rootAssignments?.find((item) => item.skillId === skill.skillId)?.rootId) || skill.tag;
|
||||
return data.roots?.some((root) => root.id === id && root.enabled) ? id : "__unassigned__";
|
||||
}
|
||||
export function groupCuratedSkills(skills: Skill[], data: SkillCuration) {
|
||||
return (data.roots || []).map((root) => ({ value: root.id, label: root.name, skills: skills.filter((skill) => effectiveCurationRoot(skill, data) === root.id) })).filter((group) => group.skills.length);
|
||||
}
|
||||
|
||||
export function curationQuery(data: SkillCuration | null, value: string) {
|
||||
if (!data?.enabled || !value) return {};
|
||||
if (value.startsWith("root:")) return { platformRootId: value.slice(5) };
|
||||
return value === "__uncategorized__" ? { platformUncategorized: true } : { platformCategoryId: value };
|
||||
}
|
||||
|
||||
@@ -30,18 +44,21 @@ export function useSkillCuration(active = true) {
|
||||
|
||||
export function matchesCuration(skill: Skill, data: SkillCuration | null, category: string) {
|
||||
if (!data?.enabled || !category) return true;
|
||||
if (category.startsWith("root:")) return effectiveCurationRoot(skill, data) === category.slice(5);
|
||||
const ids = data.assignments.filter((item) => item.skillId === skill.skillId).map((item) => item.categoryId);
|
||||
return category === "__uncategorized__" ? ids.length === 0 : ids.includes(category);
|
||||
}
|
||||
|
||||
export function SkillCurationBrowser({ data, value, onChange, error, retry }: { data: SkillCuration | null; value: string; onChange: (value: string) => void; error: string; retry: () => void }) {
|
||||
useEffect(() => {
|
||||
if ((!data?.enabled || (value && value !== "__uncategorized__" && !data.categories.some((item) => item.id === value))) && value) onChange("");
|
||||
const valid = value.startsWith("root:") ? data?.roots?.some((root) => root.id === value.slice(5)) : data?.categories.some((item) => item.id === value);
|
||||
if ((!data?.enabled || (value && value !== "__uncategorized__" && !valid)) && value) onChange("");
|
||||
}, [data, value, onChange]);
|
||||
if (error) return <Alert type="warning" title={error} action={<Button onClick={retry}>重试</Button>} />;
|
||||
if (!data?.enabled) return null;
|
||||
return <Space wrap><span>平台分类</span><Select aria-label="平台子分类" value={value} onChange={onChange} popupMatchSelectWidth={false} options={[
|
||||
{ value: "", label: "全部子分类" }, { value: "__uncategorized__", label: "未细分" },
|
||||
...data.categories.map((item) => ({ value: item.id, label: `${fallbackSkillCategories.find((root) => root.value === item.rootTag)?.label || item.rootTag} / ${item.name}` })),
|
||||
return <Space wrap><span>平台分类</span><Select aria-label="平台分类" value={value} onChange={onChange} popupMatchSelectWidth={false} options={[
|
||||
{ value: "", label: "全部分类" }, { value: "__uncategorized__", label: "未细分" },
|
||||
...(data.roots || []).map((root) => { const Icon = curationIcon(root.iconKey); return { value: `root:${root.id}`, label: <Space><Icon size={16} />{root.name}</Space> }; }),
|
||||
...data.categories.map((item) => ({ value: item.id, label: `${data.roots?.find((root) => root.id === item.rootTag)?.name || "分类"} / ${item.name}` })),
|
||||
]} /></Space>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user