chore(*): 代码同步 - 合并远端 main 最新变更

This commit is contained in:
ddCat
2026-09-01 16:47:22 +08:00
5 changed files with 303 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type ComponentType, type SVGProps } from "react";
import { Input, Popover } from "antd";
import { useEffect, useMemo, useRef, useState, type ComponentType, type SVGProps } from "react";
import { Button, Input, Modal } from "antd";
import { Cpu, Search, X } from "lucide-react";
import { toc } from "@lobehub/icons/es/toc";
@@ -65,51 +65,124 @@ export function ModelLogo({ icon, size = 18, className }: { icon?: string; size?
export function ModelIconPicker({ value, onChange }: { value?: string; onChange?: (value: string) => void }) {
const [open, setOpen] = useState(false);
const [keyword, setKeyword] = useState("");
const searchInputRef = useRef<any>(null);
useEffect(() => {
if (open) {
const timer = setTimeout(() => searchInputRef.current?.focus(), 100);
return () => clearTimeout(timer);
} else {
setKeyword("");
}
}, [open]);
const filteredIcons = useMemo(() => {
const query = keyword.trim().toLowerCase();
return query ? iconOptions.filter((item) => `${item.id} ${item.title}`.toLowerCase().includes(query)) : iconOptions;
}, [keyword]);
const content = (
<div className="w-full max-w-xl space-y-2" data-canvas-no-zoom>
<div className="flex items-center gap-2">
<Input size="small" prefix={<Search className="size-3.5 text-foreground/40" />} value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="搜索 Logo" allowClear />
{value ? (
<button type="button" className="inline-flex size-7 shrink-0 items-center justify-center rounded-md text-foreground/45 hover:text-foreground" onClick={() => onChange?.("")} aria-label="清除 Logo">
<X className="size-3.5" />
</button>
) : null}
</div>
<div className="grid max-h-96 grid-cols-12 gap-1 overflow-y-auto pr-1" role="listbox" aria-label="模型 Logo">
{filteredIcons.map((item) => {
const selected = value === item.id;
return (
<button
key={item.id}
type="button"
role="option"
aria-selected={selected}
title={item.title}
className={cn("flex size-10 items-center justify-center rounded-md text-foreground/75 hover:bg-muted/40", selected && "bg-muted/60 text-foreground")}
onClick={() => {
onChange?.(item.id);
setOpen(false);
}}
>
<ModelLogo icon={item.id} size={20} />
</button>
);
})}
</div>
</div>
);
const selectedOption = useMemo(() => (value ? iconOptions.find((item) => item.id === value) : undefined), [value]);
return (
<Popover trigger="click" open={open} onOpenChange={setOpen} arrow={{ pointAtCenter: true }} placement="bottomLeft" classNames={{ root: "model-logo-picker-popover" }} content={content}>
<button type="button" className="flex min-h-9 w-full items-center gap-2 rounded-md border border-border/60 bg-background px-2.5 text-left text-sm hover:bg-muted/20" aria-label="选择模型 Logo">
<>
<button
type="button"
className="flex min-h-9 w-full items-center gap-2 rounded-md border border-border/60 bg-background px-2.5 text-left text-sm transition-colors hover:border-border hover:bg-muted/20 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1"
aria-label="选择模型 Logo"
onClick={() => setOpen(true)}
>
<ModelLogo icon={value} size={20} />
<span className="min-w-0 flex-1 truncate text-foreground/70">{value ? iconOptions.find((item) => item.id === value)?.title || value : "选择 Logo"}</span>
<span className="min-w-0 flex-1 truncate text-foreground/70">{selectedOption?.title || value || "选择 Logo"}</span>
{value ? (
<span
className="ml-auto inline-flex size-5 shrink-0 items-center justify-center rounded text-foreground/40 hover:bg-muted hover:text-foreground"
onClick={(event) => {
event.stopPropagation();
onChange?.("");
}}
title="清除 Logo"
>
<X className="size-3" />
</span>
) : (
<span className="text-xs text-foreground/35">浏览</span>
)}
</button>
</Popover>
<Modal
title="选择模型 Logo"
open={open}
onCancel={() => setOpen(false)}
width={540}
centered
destroyOnClose
footer={
<div className="flex items-center justify-between py-1 text-xs text-foreground/45">
<span>共 {filteredIcons.length} 个可用 Logo</span>
<div className="flex items-center gap-2">
{value ? (
<Button
size="small"
onClick={() => {
onChange?.("");
setOpen(false);
}}
>
清除 Logo
</Button>
) : null}
<Button type="primary" size="small" onClick={() => setOpen(false)}>
完成
</Button>
</div>
</div>
}
>
<div className="space-y-3 pt-2">
<Input
ref={searchInputRef}
prefix={<Search className="size-4 text-foreground/40" />}
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索品牌或模型名称(如 OpenAI, Claude, Google, Flux...)"
allowClear
/>
<div
className="grid max-h-[380px] grid-cols-8 gap-2 overflow-y-auto pr-1 sm:grid-cols-9"
role="listbox"
aria-label="模型 Logo 列表"
>
{filteredIcons.length ? (
filteredIcons.map((item) => {
const selected = value === item.id;
return (
<button
key={item.id}
type="button"
role="option"
aria-selected={selected}
title={item.title}
className={cn(
"group relative flex size-12 flex-col items-center justify-center rounded-lg border border-border/50 bg-surface text-foreground/75 transition-all hover:scale-105 hover:border-primary/60 hover:bg-surface-active hover:text-foreground",
selected && "border-primary bg-primary/10 text-primary shadow-xs ring-1 ring-primary"
)}
onClick={() => {
onChange?.(item.id);
setOpen(false);
}}
>
<ModelLogo icon={item.id} size={24} />
</button>
);
})
) : (
<div className="col-span-full py-12 text-center text-xs text-foreground/45">
未找到与 “{keyword}” 相关的 Logo
</div>
)}
</div>
</div>
</Modal>
</>
);
}

View File

@@ -15,7 +15,7 @@ describe("章节分镜写入分镜制作", () => {
expect(shots).toHaveLength(1);
expect(shots[0]).toMatchObject({
title: "SC.01",
title: "镜头01",
description: "林默推门进入雨夜的旧屋。",
durationMs: 5000,
revision: {