wip: 合并 master 前暂存本地改动

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-10 08:36:47 +08:00
1 parent 1ce15fbb15
commit 44c0f6189b
13 files changed
+712 -444

No files matched your search

@@ -23,6 +23,7 @@ import {
TrendingUpIcon,
Upload,
UserIcon,
XIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -154,6 +155,11 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
const [tagIds, setTagIds] = useState<string[]>([]);
const [sort, setSort] = useState<SkillSort>("time_desc");
const [page, setPage] = useState(1);
// Publisher filter: click a skill's 发布者 to keep only that user's skills.
// {id} is owner_user_id; {label} the desensitized display name.
const [publisher, setPublisher] = useState<{ id: string; label: string } | null>(
null,
);
const [viewMode, setViewMode] = useViewMode("skill");
const reconcileMutation = useReconcileCustomSkills();
const fillNamesMutation = useGenerateMissingSkillNameZh();
@@ -203,7 +209,14 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
};
}, [skills, currentUserId]);
const filteredSkills = filter === "mine" ? activeMine : activeAll;
const scopedSkills = filter === "mine" ? activeMine : activeAll;
const filteredSkills = useMemo(
() =>
publisher
? scopedSkills.filter((s) => s.owner_user_id === publisher.id)
: scopedSkills,
[scopedSkills, publisher],
);
const orderedSkills = useMemo(
() =>
filteredSkills
@@ -228,7 +241,7 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
}, [page, totalPages]);
useEffect(() => {
setPage(1);
}, [search, tagIds, sort]);
}, [search, tagIds, sort, publisher?.id, filter]);
const pagedSkills = useMemo(
() => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
[orderedSkills, page],
@@ -344,12 +357,13 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
<Button
size="sm"
variant="outline"
className="gap-1.5 border-violet-300 bg-violet-50 font-medium text-violet-700 hover:bg-violet-100 hover:text-violet-800 dark:border-violet-500/40 dark:bg-violet-500/15 dark:text-violet-300 dark:hover:bg-violet-500/25"
onClick={() => void handleFillNames()}
disabled={
fillNamesMutation.isPending ||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
}
title="为所有尚未配置中文名称的技能自动生成中文名"
title="用大模型为所有尚未配置中文名称的技能自动生成中文名"
>
{fillNamesMutation.isPending ? (
<Loader2 className="size-4 animate-spin" />
@@ -399,6 +413,20 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
</div>
}
/>
{publisher && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">筛选发布者:</span>
<button
type="button"
onClick={() => setPublisher(null)}
className="bg-primary/10 text-primary hover:bg-primary/20 inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors"
title="点击清除发布者筛选"
>
{publisher.label}
<XIcon className="size-3.5" />
</button>
</div>
)}
{isLoading && (
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
)}
@@ -421,6 +449,7 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
currentUserId={currentUserId}
isAdmin={isAdmin}
layout={viewMode === "card" ? "card" : "row"}
onPublisherClick={setPublisher}
/>
))}
</div>
@@ -478,12 +507,16 @@ export function SkillRow({
currentUserId,
isAdmin,
layout = "row",
onPublisherClick,
}: {
skill: Skill;
currentUserId: string | null;
isAdmin: boolean;
/** "row" = full-width horizontal item (default); "card" = grid card cell. */
layout?: "row" | "card";
/** Click the 发布者 line to filter the list to that publisher. Receives the
* owner_user_id + its desensitized display label. */
onPublisherClick?: (publisher: { id: string; label: string }) => void;
}) {
const isCard = layout === "card";
const { t } = useI18n();
@@ -674,10 +707,31 @@ export function SkillRow({
const metaNodes = (
<>
{!isBuiltin && skill.owner_username && (
<div className="text-muted-foreground mt-1 flex items-center gap-1 text-xs">
<UserIcon className="size-3" />
发布者:{desensitize(skill.owner_username)}
</div>
<button
type="button"
disabled={!skill.owner_user_id || !onPublisherClick}
onClick={(e) => {
if (!skill.owner_user_id || !onPublisherClick) return;
e.stopPropagation();
onPublisherClick({
id: skill.owner_user_id,
label: desensitize(skill.owner_username!),
});
}}
className={cn(
"text-muted-foreground mt-1 flex max-w-full items-center gap-1 text-sm font-medium",
skill.owner_user_id && onPublisherClick &&
"hover:text-primary cursor-pointer transition-colors hover:underline",
)}
title={
skill.owner_user_id && onPublisherClick
? `点击筛选发布者:${desensitize(skill.owner_username)}`
: `发布者:${desensitize(skill.owner_username)}`
}
>
<UserIcon className="size-3.5 shrink-0" />
<span className="truncate">发布者:{desensitize(skill.owner_username)}</span>
</button>
)}
{skill.use_count !== undefined && (
<div className="text-muted-foreground mt-1 text-xs">