forked from EduCraft/curriculum-project-hub
83 lines
3.2 KiB
Svelte
83 lines
3.2 KiB
Svelte
<script lang="ts">
|
|
import TreeNode from "./TreeNode.svelte";
|
|
import { api } from "./api.js";
|
|
import { expanded, currentNode, breadcrumb, toggleExpanded, treeVersion } from "./browser.js";
|
|
import { toastErr } from "./stores.js";
|
|
import type { BreadcrumbEntry, NodeChild, NodeDetail } from "./types.js";
|
|
|
|
let { node, depth }: { node: NodeChild; depth: number } = $props();
|
|
|
|
let children = $state<NodeChild[] | null>(null);
|
|
const isOpen = $derived($expanded.has(node.id));
|
|
const isSelected = $derived($currentNode?.id === node.id);
|
|
|
|
// 树刷新信号(增/删/移/重命名)→ 失效子节点缓存,展开状态下随之重载
|
|
$effect(() => {
|
|
void $treeVersion;
|
|
children = null;
|
|
});
|
|
|
|
$effect(() => {
|
|
if (isOpen && node.kind === "FOLDER" && children === null) {
|
|
api<{ nodes: NodeChild[] }>(`/database/api/nodes?parentId=${encodeURIComponent(node.id)}`)
|
|
.then((r) => (children = r.nodes))
|
|
.catch((e) => toastErr(e instanceof Error ? e.message : String(e)));
|
|
}
|
|
});
|
|
|
|
async function select(): Promise<void> {
|
|
if (node.kind === "FOLDER") toggleExpanded(node.id);
|
|
try {
|
|
const [detail, crumb] = await Promise.all([
|
|
api<{ node: NodeDetail }>(`/database/api/nodes/${node.id}`),
|
|
api<{ breadcrumb: BreadcrumbEntry[] }>(`/database/api/nodes/${node.id}/breadcrumb`),
|
|
]);
|
|
currentNode.set(detail.node);
|
|
breadcrumb.set(crumb.breadcrumb);
|
|
} catch (e) {
|
|
toastErr(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div>
|
|
<div
|
|
class="tree-item flex cursor-pointer items-center gap-1 rounded-lg px-1.5 py-1.5 select-none {isSelected ? 'bg-selected' : 'hover:bg-hover'}"
|
|
role="button"
|
|
tabindex="0"
|
|
onclick={select}
|
|
onkeydown={(e) => e.key === "Enter" && select()}
|
|
>
|
|
<span class="flex h-4 w-4 shrink-0 items-center justify-center text-ink-3">
|
|
{#if node.kind === "FOLDER"}
|
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor">
|
|
{#if isOpen}<path d="M6 9l6 6 6-6z" />{:else}<path d="M9 6l6 6-6 6z" />{/if}
|
|
</svg>
|
|
{/if}
|
|
</span>
|
|
<span class="flex h-4 w-4 shrink-0 items-center justify-center {node.kind === 'PROJECT' ? 'text-ink' : 'text-ink-3'}">
|
|
{#if node.kind === "PROJECT"}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
|
{:else}
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" /></svg>
|
|
{/if}
|
|
</span>
|
|
<span class="truncate">{node.name}</span>
|
|
{#if node.role !== "MANAGE"}
|
|
<span class="ml-auto pr-1 font-mono text-[10px] text-ink-3">{node.role}</span>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if node.kind === "FOLDER" && isOpen}
|
|
<div class="ml-[15px] border-l border-guide pl-1">
|
|
{#if children === null}
|
|
<div class="px-3 py-1.5 text-xs text-ink-3">…</div>
|
|
{:else}
|
|
{#each children as child (child.id)}
|
|
<TreeNode node={child} depth={depth + 1} />
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|