Files
curriculum-project-hub/hub/filelib-web/src/lib/OverviewPanel.svelte
T
ymy be17f74fc2 chore(filelib-web): 文件库权限名称汉化(VIEW/EDIT/MANAGE → 只读/可编辑/可管理)
统一走新增共享常量 labels.ts ROLE_LABEL(与 OverviewPanel 既有文案一致);
覆盖授权表格与弹窗下拉、详情头 tag、树节点角标;API 传参仍用英文枚举。
2026-07-27 15:57:41 +08:00

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}