161 lines
6.3 KiB
TypeScript
161 lines
6.3 KiB
TypeScript
import { memo, useMemo, useState } from "react";
|
|
|
|
import { type ScheduleArtifact } from "../../services/api";
|
|
import Icon from "../../components/common/Icon";
|
|
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="schedule-panel schedule-artifacts-panel">
|
|
<div className="schedule-panel__title">
|
|
<div>
|
|
<strong>稳定版本脚本</strong>
|
|
<span className="schedule-panel__count">{artifacts.length}</span>
|
|
</div>
|
|
</div>
|
|
<label className="schedule-search">
|
|
<Icon name="search" size={15} />
|
|
<input
|
|
value={keyword}
|
|
placeholder="搜索稳定版本"
|
|
onChange={(event) => onKeywordChange(event.target.value)}
|
|
/>
|
|
</label>
|
|
<div className="artifact-list">
|
|
{groups.length === 0 ? (
|
|
<p className="schedule-empty">
|
|
暂无稳定版本,请先在"构建脚本"中发布
|
|
</p>
|
|
) : (
|
|
groups.map(([scriptName, items]) => {
|
|
const isExpanded = expandedScriptNames.has(scriptName);
|
|
const scriptType = items[0].script_type;
|
|
const iconName = scriptType === "notebook" ? "notebook" : "python";
|
|
return (
|
|
<div key={scriptName} className="artifact-group">
|
|
<button
|
|
type="button"
|
|
className="artifact-group__header"
|
|
aria-expanded={isExpanded}
|
|
onClick={() => toggle(scriptName)}
|
|
>
|
|
<span className="artifact-group__main">
|
|
<span className={`artifact-group__chevron${isExpanded ? " is-open" : ""}`}>
|
|
<Icon name="chevron" size={14} />
|
|
</span>
|
|
<span className={`artifact-group__icon artifact-group__icon--${scriptType}`}>
|
|
<Icon name={iconName} size={14} />
|
|
</span>
|
|
<span className="artifact-group__name" title={scriptName}>
|
|
{scriptName}
|
|
</span>
|
|
</span>
|
|
{items.length > 1 && (
|
|
<span className="artifact-group__count">{items.length}</span>
|
|
)}
|
|
</button>
|
|
{isExpanded && (
|
|
<div className="artifact-versions">
|
|
{items.map((artifact) => (
|
|
<div
|
|
className="artifact-version"
|
|
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="artifact-version__main">
|
|
<span className="artifact-version__handle">
|
|
<Icon name="grip-vertical" size={13} />
|
|
</span>
|
|
<span className="artifact-version__name">
|
|
{artifact.version_label}
|
|
</span>
|
|
<span className="artifact-version__commit">
|
|
{shortHash(artifact.content_hash)}
|
|
</span>
|
|
</span>
|
|
<span className="artifact-version__action">
|
|
<Icon name="external" size={12} />
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
<p className="artifact-tip artifact-tip--footer">
|
|
<span>按住</span>
|
|
<span className="artifact-tip__grip">
|
|
<Icon name="grip-vertical" size={12} />
|
|
</span>
|
|
<span>拖拽卡片到画布即可绑定节点</span>
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export const ArtifactList = memo(ArtifactListImpl); |