forked from EduCraft/curriculum-project-hub
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user