forked from EduCraft/curriculum-project-hub
be17f74fc2
统一走新增共享常量 labels.ts ROLE_LABEL(与 OverviewPanel 既有文案一致); 覆盖授权表格与弹窗下拉、详情头 tag、树节点角标;API 传参仍用英文枚举。
157 lines
5.6 KiB
Svelte
157 lines
5.6 KiB
Svelte
<script lang="ts">
|
|
import { api } from "./api.js";
|
|
import { toastOk, toastErr } from "./stores.js";
|
|
import { currentNode } from "./browser.js";
|
|
import { ROLE_LABEL } from "./labels.js";
|
|
import type { ExportJob, NodeDetail } from "./types.js";
|
|
import Modal from "./Modal.svelte";
|
|
|
|
let { node }: { node: NodeDetail } = $props();
|
|
|
|
let showEditDesc = $state(false);
|
|
let descDraft = $state("");
|
|
let exportJob = $state<ExportJob | null>(null);
|
|
|
|
const canEdit = $derived(node.role === "MANAGE" || node.role === "EDIT");
|
|
const canManage = $derived(node.role === "MANAGE");
|
|
const roleLabel = $derived(ROLE_LABEL[node.role]);
|
|
|
|
/** 独立权限开关(仅 PROJECT;关闭时只继承父级权限,创建者除外)。 */
|
|
async function toggleIndependent(): Promise<void> {
|
|
try {
|
|
await api(`/database/api/projects/${node.id}/independent-permission`, {
|
|
method: "PUT",
|
|
body: { enabled: !node.independentPermission },
|
|
});
|
|
toastOk("已切换");
|
|
currentNode.update((n) =>
|
|
n !== null && n.id === node.id ? { ...n, independentPermission: !node.independentPermission } : n,
|
|
);
|
|
} catch (e) {
|
|
toastErr(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
void node.id;
|
|
exportJob = null;
|
|
});
|
|
|
|
function openEditDesc(): void {
|
|
descDraft = node.description ?? "";
|
|
showEditDesc = true;
|
|
}
|
|
|
|
async function saveDesc(): Promise<void> {
|
|
const description = descDraft.trim();
|
|
try {
|
|
await api(`/database/api/nodes/${node.id}`, {
|
|
method: "PATCH",
|
|
body: { description: description === "" ? null : description },
|
|
});
|
|
toastOk("简介已保存");
|
|
showEditDesc = false;
|
|
const next = description === "" ? null : description;
|
|
currentNode.update((n) => (n && n.id === node.id ? { ...n, description: next } : n));
|
|
} catch (e) {
|
|
toastErr(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
|
|
async function submitExport(): Promise<void> {
|
|
try {
|
|
const r = await api<{ jobId: string; status: string }>(`/database/api/projects/${node.id}/exports`, {
|
|
method: "POST",
|
|
body: { target: "manifest" },
|
|
});
|
|
toastOk("导出已提交");
|
|
void pollExport(r.jobId);
|
|
} catch (e) {
|
|
toastErr(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
|
|
async function pollExport(jobId: string): Promise<void> {
|
|
for (;;) {
|
|
await new Promise((r) => setTimeout(r, 800));
|
|
try {
|
|
const job = await api<ExportJob>(`/database/api/exports/${jobId}`);
|
|
exportJob = job;
|
|
if (job.status === "DONE" || job.status === "FAILED") break;
|
|
} catch {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="panel">
|
|
<div class="mb-4">
|
|
<div class="mb-1.5 text-[11.5px] text-ink-3">简介</div>
|
|
<div class="text-[13.5px] leading-7 text-ink">
|
|
{#if node.description}
|
|
{node.description}
|
|
{:else}
|
|
<span class="italic text-ink-3">暂无简介</span>
|
|
{/if}
|
|
{#if canEdit}
|
|
<button class="btn ml-2.5 !px-2.5 !py-0.5 align-middle !text-[11.5px]" onclick={openEditDesc}>编辑</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="my-4 border-t border-line-soft"></div>
|
|
|
|
<div class="flex flex-col gap-1.5 text-[13px] text-ink-2">
|
|
<div>类型 <b class="font-semibold text-ink">{node.kind === "PROJECT" ? "项目" : "文件夹"}</b></div>
|
|
<div>我的角色 <b class="font-semibold text-ink">{roleLabel}</b></div>
|
|
<div>创建时间 <b class="font-semibold text-ink">{new Date(node.createdAt).toLocaleString("zh-CN")}</b></div>
|
|
<div>更新时间 <b class="font-semibold text-ink">{new Date(node.updatedAt).toLocaleString("zh-CN")}</b></div>
|
|
</div>
|
|
|
|
<!-- 独立权限与导出都只对 PROJECT 有意义(FOLDER 是透明组织节点,ADR-0021)。 -->
|
|
{#if node.kind === "PROJECT"}
|
|
<div class="my-4 border-t border-line-soft"></div>
|
|
<div class="flex flex-wrap items-center gap-2.5">
|
|
<span class="quiet">独立权限</span>
|
|
<b class="text-[13px]">{node.independentPermission ? "开启" : "关闭"}</b>
|
|
{#if canManage}
|
|
<button class="btn" onclick={toggleIndependent}>{node.independentPermission ? "关闭" : "开启"}</button>
|
|
{/if}
|
|
<span class="quiet">关闭时仅继承父级权限(创建者除外)</span>
|
|
</div>
|
|
|
|
<div class="my-4 border-t border-line-soft"></div>
|
|
|
|
<div class="section-title mb-2">导出</div>
|
|
<div class="flex items-center gap-2">
|
|
<select class="select !w-auto"><option value="manifest">manifest(stub)</option></select>
|
|
<button class="btn" onclick={submitExport}>开始导出</button>
|
|
{#if exportJob}
|
|
<span class="file-meta">
|
|
{#if exportJob.status === "DONE"}
|
|
完成 · <a class="text-accent underline" href="/database/api/exports/{exportJob.id}/download">下载</a>
|
|
{:else if exportJob.status === "FAILED"}
|
|
失败:{exportJob.error ?? ""}
|
|
{:else}
|
|
{exportJob.status}…
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if showEditDesc}
|
|
<Modal title="编辑简介" onclose={() => (showEditDesc = false)}>
|
|
<div class="form-row">
|
|
<label class="form-label" for="desc-draft">简要说明这个项目的内容</label>
|
|
<textarea id="desc-draft" rows="5" class="input !leading-7" bind:value={descDraft} placeholder="例如:高中物理必修一第三章,表面张力相关内容……"></textarea>
|
|
</div>
|
|
<div class="mt-4 flex justify-end gap-2">
|
|
<button class="btn" onclick={() => (showEditDesc = false)}>取消</button>
|
|
<button class="btn btn-primary" onclick={saveDesc}>保存</button>
|
|
</div>
|
|
</Modal>
|
|
{/if}
|