fix(database): 补回文件库的「授权」tab 与 /me 的显示名

迁移时整个授权 tab 连同四个端点一起漏掉了 —— 后端一直可用,前端零调用:
  GET/PUT/DELETE /nodes/:id/grants
  PUT /projects/:id/independent-permission
权限编辑是这个后台的核心用途,而它此前在界面上完全不可达。

tab 组装也修正为与旧 libraryBrowser 一致:概览恒有、文件仅 PROJECT、
授权仅 MANAGE。注意文件夹也有授权 tab —— 它虽是透明组织节点,授权仍
挂在节点上(ADR-0021);此前文件夹一个 tab 都没有。

GrantsPanel 的语义按契约 8.1:创建者授权不给收回入口;MANAGE 仅创建者
可授,前端不拦,后端 fail closed 的报错原样呈现;GROUP 主体走
/groups/search 下拉选,不手敲 id。

/database/api/me 加 displayName 与 avatarUrl:侧栏此前显示原始 userId。
旧页面是服务端渲染,handler 里查 Prisma 就有名字;页面不再服务端渲染后
(ADR-0029),模板闭包过的数据也是被迁移的契约的一部分,不是旧实现的
无关细节。

概览面板同时补回丢失的「类型」「更新时间」两行、导出 target 下拉、
节点标题旁的角色 tag,以及整块缺失的独立权限开关。
This commit is contained in:
2026-07-26 20:19:47 +08:00
parent eeb8f56742
commit cdeb29ccf2
4 changed files with 312 additions and 58 deletions
+51 -32
View File
@@ -4,9 +4,12 @@
import { toastOk, toastErr } from "./stores.js";
import OverviewPanel from "./OverviewPanel.svelte";
import FilesPanel from "./FilesPanel.svelte";
import GrantsPanel from "./GrantsPanel.svelte";
import Modal from "./Modal.svelte";
import Icon from "./Icon.svelte";
let tab = $state<"detail" | "files">("detail");
type Tab = "detail" | "files" | "grants";
let tab = $state<Tab>("detail");
let showCreateChild = $state(false);
let newName = $state("");
let newKind = $state<"FOLDER" | "PROJECT">("FOLDER");
@@ -17,6 +20,15 @@
const canManage = $derived(node?.role === "MANAGE");
const canEdit = $derived(canManage || node?.role === "EDIT");
// 与旧 libraryBrowser 的 tab 组装一致:概览恒有;文件仅 PROJECT;授权仅 MANAGE
// (FOLDER 也有授权 —— 它虽是透明组织节点,授权仍挂在节点上,ADR-0021)。
const tabs = $derived.by((): ReadonlyArray<readonly [Tab, string]> => {
const out: Array<readonly [Tab, string]> = [["detail", "概览"]];
if (node?.kind === "PROJECT") out.push(["files", "文件"]);
if (canManage) out.push(["grants", "授权"]);
return out;
});
$effect(() => {
void node?.id;
tab = "detail";
@@ -84,36 +96,43 @@
</div>
<div class="mb-5 flex items-center justify-between">
<div class="flex items-center gap-2.5 text-[19px] font-semibold text-ink">
<div class="flex items-center gap-2 text-[17px] 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>
<span class="tag">{node.kind === "PROJECT" ? "项目" : "文件夹"}</span>
<span class="tag !border-line !text-ink-2">{node.role}</span>
</div>
<div class="flex gap-2">
<div class="flex gap-1.5">
{#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>
<button class="btn" onclick={() => (showCreateChild = true)}>
<Icon name="plus" size={13} /> 新建子节点
</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>
<button class="btn" onclick={renameNode}><Icon name="pencil" size={13} /> 重命名</button>
<button class="btn btn-danger" onclick={deleteNode}><Icon name="trash" size={13} /> 删除</button>
{/if}
</div>
</div>
{#if node.kind === "FOLDER"}
<div class="py-7 text-[13px] text-ink-3">点击左侧树展开以浏览子内容</div>
<div class="mb-[18px] flex gap-0.5 border-b border-line-soft">
{#each tabs as [id, label] (id)}
<button
class="-mb-px 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)}
>{label}</button>
{/each}
</div>
{#if tab === "grants"}
<GrantsPanel {node} />
{:else if tab === "files" && node.kind === "PROJECT"}
<FilesPanel {node} />
{: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} />
<OverviewPanel {node} />
{#if node.kind === "FOLDER"}
<div class="quiet mt-3.5">文件夹是透明组织节点,点左侧树展开以浏览子内容。</div>
{/if}
{/if}
</div>
@@ -121,24 +140,24 @@
{#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 class="form-row">
<label class="form-label" for="child-name">名称</label>
<input id="child-name" class="input" 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}>
<div class="form-row">
<label class="form-label" for="child-kind">类型</label>
<select id="child-kind" class="select" 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 class="form-row">
<label class="form-label" for="child-desc">简介(可选)</label>
<textarea id="child-desc" rows="3" class="textarea" 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>
<button class="btn" onclick={() => (showCreateChild = false)}>取消</button>
<button class="btn btn-primary" onclick={createChild}>创建</button>
</div>
</Modal>
{/if}