192 lines
8.7 KiB
TypeScript
192 lines
8.7 KiB
TypeScript
import { memo, useMemo, useState } from "react";
|
|
|
|
import { type ScheduleArtifact } from "~/services/api";
|
|
import {
|
|
BookText,
|
|
ChevronRight,
|
|
ExternalLink,
|
|
FileCode2,
|
|
GripVertical,
|
|
Search,
|
|
} from "lucide-react";
|
|
import { shortHash } from "./utils";
|
|
|
|
// 包裹 memo 让父级 SchedulePage 在画布交互(setSelectedNodeId /
|
|
// setSelectedEdgeId / linkSourceId 等)重渲染时,artifact 卡片不会跟着
|
|
// 重建/重绑拖拽事件 —— 减少拖动过程中的事件重绑与列表抖动。
|
|
function ArtifactListImpl({
|
|
artifacts,
|
|
keyword,
|
|
onDrop,
|
|
onDoubleClick,
|
|
onContextMenu,
|
|
onKeywordChange,
|
|
}: {
|
|
artifacts: ScheduleArtifact[];
|
|
keyword: string;
|
|
onDrop: (artifact: ScheduleArtifact, x: number, y: number) => void;
|
|
onDoubleClick: (artifact: ScheduleArtifact) => void;
|
|
onContextMenu: (event: React.MouseEvent, artifact: ScheduleArtifact) => void;
|
|
onKeywordChange: (keyword: string) => void;
|
|
}) {
|
|
const filtered = keyword.trim()
|
|
? artifacts.filter((item) => (
|
|
item.script_name.toLowerCase().includes(keyword.trim().toLowerCase())
|
|
|| item.version_label.toLowerCase().includes(keyword.trim().toLowerCase())
|
|
))
|
|
: artifacts;
|
|
|
|
const [expandedScriptNames, setExpandedScriptNames] = useState<Set<string>>(new Set());
|
|
|
|
const groups = useMemo(() => {
|
|
const map = new Map<string, ScheduleArtifact[]>();
|
|
for (const item of filtered) {
|
|
const list = map.get(item.script_name);
|
|
if (list) list.push(item);
|
|
else map.set(item.script_name, [item]);
|
|
}
|
|
// 组内按 created_at desc,version_label desc 兜底
|
|
for (const list of map.values()) {
|
|
list.sort((a, b) => {
|
|
if (a.created_at !== b.created_at) return a.created_at < b.created_at ? 1 : -1;
|
|
return a.version_label < b.version_label ? 1 : -1;
|
|
});
|
|
}
|
|
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
}, [filtered]);
|
|
|
|
function toggle(scriptName: string) {
|
|
setExpandedScriptNames((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(scriptName)) next.delete(scriptName);
|
|
else next.add(scriptName);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<section className="flex min-h-0 flex-col overflow-hidden">
|
|
<div className="flex min-h-[48px] items-center justify-between px-3">
|
|
<div className="flex items-center gap-[7px]">
|
|
<strong className="text-[14px] text-[#223247]">稳定版本脚本</strong>
|
|
<span className="inline-grid h-[20px] min-w-[20px] place-items-center rounded-[10px] bg-[#eef3f8] px-[5px] text-[10px] text-[#718195]">
|
|
{artifacts.length}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<label className="mx-[10px] mb-[7px] flex min-h-[35px] items-center gap-[7px] rounded-[6px] border border-line bg-white px-[9px] text-[#95a3b2] focus-within:border-[#8db9e8] focus-within:ring-3 focus-within:ring-brand/10">
|
|
<Search size={15} />
|
|
<input
|
|
className="min-w-0 flex-1 border-0 bg-transparent text-[12px] text-[#33475d] outline-none placeholder:text-[#a6b2be]"
|
|
value={keyword}
|
|
placeholder="搜索稳定版本"
|
|
onChange={(event) => onKeywordChange(event.target.value)}
|
|
/>
|
|
</label>
|
|
<div className="scrollbar-thin min-h-0 overflow-auto px-2 pb-[9px]">
|
|
{groups.length === 0 ? (
|
|
<p className="my-[18px_10px] text-center text-[10px] leading-[1.6] text-[#9aa6b3]">
|
|
暂无稳定版本,请先在"构建脚本"中发布
|
|
</p>
|
|
) : (
|
|
groups.map(([scriptName, items]) => {
|
|
const isExpanded = expandedScriptNames.has(scriptName);
|
|
const scriptType = items[0].script_type;
|
|
return (
|
|
<div
|
|
key={scriptName}
|
|
className="mb-1.5 overflow-hidden rounded-[8px] border border-line-soft bg-white transition-[border-color,box-shadow] duration-150 hover:border-[#e1e8ef]"
|
|
>
|
|
<button
|
|
type="button"
|
|
className="flex min-h-[42px] w-full cursor-pointer select-none items-center justify-between gap-2 border-0 bg-transparent px-[7px] py-[7px] text-left text-inherit hover:bg-[#f8fafc] min-[1281px]:px-2"
|
|
aria-expanded={isExpanded}
|
|
onClick={() => toggle(scriptName)}
|
|
>
|
|
<span className="flex min-w-0 items-center gap-[7px]">
|
|
<span
|
|
data-state={isExpanded ? "open" : "closed"}
|
|
className="h-[14px] w-[14px] flex-none text-[#9aa7b5] transition-transform duration-200 data-[state=open]:rotate-90"
|
|
>
|
|
<ChevronRight size={14} />
|
|
</span>
|
|
<span
|
|
className={`grid h-[27px] w-[27px] flex-none place-items-center rounded-[6px] ${
|
|
scriptType === "notebook"
|
|
? "bg-[#fff0ee] text-[#dc5c52]"
|
|
: "bg-[#eaf3ff] text-[#326fc1]"
|
|
}`}
|
|
>
|
|
{scriptType === "notebook" ? (
|
|
<BookText size={14} />
|
|
) : (
|
|
<FileCode2 size={14} />
|
|
)}
|
|
</span>
|
|
<span
|
|
className="max-w-[125px] min-w-0 truncate text-[11px] font-semibold text-[#33465a] min-[1281px]:max-w-none"
|
|
title={scriptName}
|
|
>
|
|
{scriptName}
|
|
</span>
|
|
</span>
|
|
{items.length > 1 && (
|
|
<span className="inline-flex flex-none items-center rounded-full border border-[#e5eaf0] bg-[#f5f7fa] px-1.5 py-[2px] text-[9px] leading-[1.3] whitespace-nowrap text-[#8794a3]">
|
|
{items.length}
|
|
</span>
|
|
)}
|
|
</button>
|
|
{isExpanded && (
|
|
<div className="flex flex-col gap-[5px] border-t border-line-soft bg-[#f8fafc] p-[7px_7px_7px_27px] min-[1281px]:pl-[29px]">
|
|
{items.map((artifact) => (
|
|
<div
|
|
className="group flex min-h-[34px] cursor-grab select-none items-center justify-between gap-[7px] rounded-[6px] border border-[#e4e9ee] bg-white p-1.5 transition-[border-color,background-color] duration-150 hover:border-[#a8c8e9] hover:bg-[#fbfdff] active:cursor-grabbing min-[1281px]:px-[7px]"
|
|
draggable
|
|
key={artifact.versions_id}
|
|
onDragStart={(event) => {
|
|
event.dataTransfer.effectAllowed = "copy";
|
|
event.dataTransfer.setData(
|
|
"application/x-model-platform-version",
|
|
artifact.versions_id,
|
|
);
|
|
event.dataTransfer.setData("text/plain", artifact.versions_id);
|
|
}}
|
|
onContextMenu={(event) => onContextMenu(event, artifact)}
|
|
onDoubleClick={() => onDoubleClick(artifact)}
|
|
>
|
|
<span className="flex min-w-0 items-center gap-[5px]">
|
|
<span className="h-[13px] w-[13px] flex-none text-[#c4cdd6] group-hover:text-[#7e8d9d]">
|
|
<GripVertical size={13} />
|
|
</span>
|
|
<span className="flex-none text-[10px] font-semibold text-[#526477]">
|
|
{artifact.version_label}
|
|
</span>
|
|
<span className="min-w-0 truncate font-mono text-[8px] text-[#9aa5b1]">
|
|
{shortHash(artifact.content_hash)}
|
|
</span>
|
|
</span>
|
|
<span className="grid h-[18px] w-[18px] flex-none place-items-center rounded-[4px] text-[#c1cad3] group-hover:bg-[#eef6ff] group-hover:text-[#287ac5]">
|
|
<ExternalLink size={12} />
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
<p className="mx-[10px] mb-[9px] mt-[3px] flex items-center justify-center gap-1 rounded-[6px] border border-line-soft bg-[#f8fafc] p-[7px_8px] text-center text-[9px] leading-[1.45] text-[#8795a4]">
|
|
<span>按住</span>
|
|
<span className="inline-flex items-center justify-center text-[#9eabb8]">
|
|
<GripVertical size={12} />
|
|
</span>
|
|
<span>拖拽卡片到画布即可绑定节点</span>
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export const ArtifactList = memo(ArtifactListImpl);
|