forked from EduCraft/curriculum-project-hub
145 lines
6.0 KiB
Svelte
145 lines
6.0 KiB
Svelte
<script lang="ts">
|
|
import { api } from "./api.js";
|
|
import { currentNode, breadcrumb, bumpTree, clearSelectedFile } from "./browser.js";
|
|
import { toastOk, toastErr } from "./stores.js";
|
|
import OverviewPanel from "./OverviewPanel.svelte";
|
|
import FilesPanel from "./FilesPanel.svelte";
|
|
import Modal from "./Modal.svelte";
|
|
|
|
let tab = $state<"detail" | "files">("detail");
|
|
let showCreateChild = $state(false);
|
|
let newName = $state("");
|
|
let newKind = $state<"FOLDER" | "PROJECT">("FOLDER");
|
|
let newDesc = $state("");
|
|
|
|
const node = $derived($currentNode);
|
|
const crumbs = $derived($breadcrumb);
|
|
const canManage = $derived(node?.role === "MANAGE");
|
|
const canEdit = $derived(canManage || node?.role === "EDIT");
|
|
|
|
$effect(() => {
|
|
void node?.id;
|
|
tab = "detail";
|
|
clearSelectedFile();
|
|
});
|
|
|
|
async function createChild(): Promise<void> {
|
|
const name = newName.trim();
|
|
if (name === "" || node === null) return;
|
|
try {
|
|
await api("/database/api/nodes", {
|
|
method: "POST",
|
|
body: {
|
|
parentId: node.id,
|
|
kind: newKind,
|
|
name,
|
|
...(newDesc.trim() !== "" ? { description: newDesc.trim() } : {}),
|
|
},
|
|
});
|
|
toastOk("已创建");
|
|
showCreateChild = false;
|
|
newName = ""; newKind = "FOLDER"; newDesc = "";
|
|
bumpTree();
|
|
} catch (e) {
|
|
toastErr(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
|
|
async function renameNode(): Promise<void> {
|
|
if (node === null) return;
|
|
const name = prompt("新名称", node.name);
|
|
if (name === null) return;
|
|
try {
|
|
await api(`/database/api/nodes/${node.id}`, { method: "PATCH", body: { name } });
|
|
toastOk("已重命名");
|
|
bumpTree();
|
|
currentNode.update((n) => (n ? { ...n, name } : n));
|
|
} catch (e) {
|
|
toastErr(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
|
|
async function deleteNode(): Promise<void> {
|
|
if (node === null || !confirm(`确认删除「${node.name}」?软删除后不可见。`)) return;
|
|
try {
|
|
await api(`/database/api/nodes/${node.id}`, { method: "DELETE" });
|
|
toastOk("已删除");
|
|
currentNode.set(null);
|
|
bumpTree();
|
|
} catch (e) {
|
|
toastErr(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if node === null}
|
|
<div class="flex h-full items-center justify-center text-[13px] text-ink-3">从左侧选择一个文件夹或项目</div>
|
|
{:else}
|
|
<div class="mx-auto max-w-[880px] px-9 py-9">
|
|
<div class="mb-2 text-[12.5px] text-ink-3">
|
|
{#each crumbs as c, i (i)}
|
|
{#if i > 0}<span class="mx-1 text-line">/</span>{/if}
|
|
<span>{c.name ?? "…"}</span>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="mb-5 flex items-center justify-between">
|
|
<div class="flex items-center gap-2.5 text-[19px] font-semibold text-ink">
|
|
{node.name}
|
|
<span class="rounded-full border border-line-soft bg-panel px-2 py-0.5 font-mono text-[10.5px] text-ink-3">{node.kind === "PROJECT" ? "项目" : "文件夹"}</span>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
{#if canEdit && node.kind === "FOLDER"}
|
|
<button class="rounded-lg border border-line bg-panel px-3 py-1.5 text-[12.5px] font-medium text-ink transition hover:bg-hover" onclick={() => (showCreateChild = true)}>+ 新建</button>
|
|
{/if}
|
|
{#if canManage}
|
|
<button class="rounded-lg border border-line bg-panel px-3 py-1.5 text-[12.5px] font-medium text-ink transition hover:bg-hover" onclick={renameNode}>重命名</button>
|
|
<button class="rounded-lg border border-transparent px-3 py-1.5 text-[12.5px] font-medium text-danger transition hover:bg-[#A13A3312]" onclick={deleteNode}>删除</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#if node.kind === "FOLDER"}
|
|
<div class="py-7 text-[13px] text-ink-3">点击左侧树展开以浏览子内容</div>
|
|
{:else}
|
|
<div class="mb-5 flex gap-1 border-b border-line-soft">
|
|
{#each [["detail", "概览"], ["files", "文件"]] as [id, label] (id)}
|
|
<button
|
|
class="border-b-2 px-3.5 py-2 text-[13px] transition {tab === id ? 'border-accent font-semibold text-ink' : 'border-transparent text-ink-3 hover:text-ink'}"
|
|
onclick={() => (tab = id as "detail" | "files")}
|
|
>{label}</button>
|
|
{/each}
|
|
</div>
|
|
{#if tab === "detail"}
|
|
<OverviewPanel {node} />
|
|
{:else}
|
|
<FilesPanel {node} />
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if showCreateChild && node}
|
|
<Modal title="新建子节点" onclose={() => (showCreateChild = false)}>
|
|
<div class="mb-3">
|
|
<label class="mb-1 block text-[11.5px] text-ink-3" for="child-name">名称</label>
|
|
<input id="child-name" class="w-full rounded-lg border border-line bg-panel px-3 py-2 text-[13px] outline-none focus:border-accent" bind:value={newName} placeholder="例如:物理必修一" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="mb-1 block text-[11.5px] text-ink-3" for="child-kind">类型</label>
|
|
<select id="child-kind" class="w-full rounded-lg border border-line bg-panel px-3 py-2 text-[13px] outline-none focus:border-accent" bind:value={newKind}>
|
|
<option value="FOLDER">文件夹</option>
|
|
<option value="PROJECT">项目(课程资源库)</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="mb-1 block text-[11.5px] text-ink-3" for="child-desc">简介(可选)</label>
|
|
<textarea id="child-desc" rows="3" class="w-full rounded-lg border border-line bg-panel px-3 py-2 font-mono text-xs outline-none focus:border-accent" bind:value={newDesc} placeholder="简要说明用途…"></textarea>
|
|
</div>
|
|
<div class="mt-4 flex justify-end gap-2">
|
|
<button class="rounded-lg border border-line bg-panel px-3.5 py-1.5 text-[12.5px] font-medium text-ink transition hover:bg-hover" onclick={() => (showCreateChild = false)}>取消</button>
|
|
<button class="rounded-lg bg-accent px-3.5 py-1.5 text-[12.5px] font-medium text-white transition hover:bg-accent-hover" onclick={createChild}>创建</button>
|
|
</div>
|
|
</Modal>
|
|
{/if}
|