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:
@@ -0,0 +1,191 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 节点授权面板。迁自已删除的 routes/libraryBrowser.ts `renderGrantsTab`(ADR-0029)。
|
||||
*
|
||||
* 迁移时整个「授权」tab 连同这四个端点一起漏掉了 —— 后端一直可用,只是前端没入口。
|
||||
*
|
||||
* 语义(契约 8.1 / ADR-0021):
|
||||
* - 创建者授权(isCreatorGrant)不可收回、不可改;
|
||||
* - MANAGE 仅创建者可授,这里不做前端拦截 —— 后端 fail closed,报错原样呈现;
|
||||
* - GROUP 主体走 in-hub MemberGroup(ADR-0028),用 /groups/search 选,不手敲 id。
|
||||
*/
|
||||
import { api } from "./api.js";
|
||||
import { toastOk, toastErr } from "./stores.js";
|
||||
import { currentNode } from "./browser.js";
|
||||
import type { Grant, MemberGroupSearchResult, NodeDetail, Role } from "./types.js";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
let { node }: { node: NodeDetail } = $props();
|
||||
|
||||
const ROLES: readonly Role[] = ["VIEW", "EDIT", "MANAGE"];
|
||||
|
||||
let grants = $state<Grant[] | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let principalType = $state<"USER" | "GROUP">("USER");
|
||||
let userIdInput = $state("");
|
||||
let groupId = $state("");
|
||||
let groupOptions = $state<MemberGroupSearchResult[] | null>(null);
|
||||
let role = $state<Role>("VIEW");
|
||||
let saving = $state(false);
|
||||
|
||||
const canManage = $derived(node.role === "MANAGE");
|
||||
const errText = (e: unknown): string => (e instanceof Error ? e.message : String(e));
|
||||
|
||||
$effect(() => {
|
||||
void node.id;
|
||||
void load();
|
||||
});
|
||||
|
||||
async function load(): Promise<void> {
|
||||
grants = null;
|
||||
error = null;
|
||||
try {
|
||||
const r = await api<{ grants: Grant[] }>(`/database/api/nodes/${node.id}/grants`);
|
||||
grants = r.grants;
|
||||
} catch (e) {
|
||||
error = errText(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 切到 GROUP 时懒加载候选组(活跃组 + breadcrumb)。 */
|
||||
async function onTypeChange(): Promise<void> {
|
||||
if (principalType !== "GROUP" || groupOptions !== null) return;
|
||||
try {
|
||||
const r = await api<{ groups: MemberGroupSearchResult[] }>("/database/api/groups/search?q=");
|
||||
groupOptions = r.groups;
|
||||
if (r.groups.length > 0 && groupId === "") groupId = r.groups[0]!.id;
|
||||
} catch (e) {
|
||||
toastErr(errText(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function addGrant(): Promise<void> {
|
||||
const principalId = principalType === "GROUP" ? groupId : userIdInput.trim();
|
||||
if (principalId === "") {
|
||||
toastErr("请填写主体");
|
||||
return;
|
||||
}
|
||||
saving = true;
|
||||
try {
|
||||
// PUT /grants 是增量语义(putGrants),不是整表替换。
|
||||
await api(`/database/api/nodes/${node.id}/grants`, {
|
||||
method: "PUT",
|
||||
body: { grants: [{ principalType, principalId, role }] },
|
||||
});
|
||||
toastOk("已授予");
|
||||
userIdInput = "";
|
||||
await load();
|
||||
} catch (e) {
|
||||
toastErr(errText(e));
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function revoke(g: Grant): Promise<void> {
|
||||
if (!confirm(`收回「${g.principalId}」的 ${g.role} 授权?`)) return;
|
||||
try {
|
||||
await api(`/database/api/nodes/${node.id}/grants/${encodeURIComponent(g.id)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
toastOk("已收回");
|
||||
await load();
|
||||
} catch (e) {
|
||||
toastErr(errText(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** 独立权限开关(仅 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,
|
||||
);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toastErr(errText(e));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel">
|
||||
{#if error !== null}
|
||||
<div class="py-2 text-[12.5px] text-danger">{error}</div>
|
||||
{:else if grants === null}
|
||||
<div class="quiet py-[18px] text-center">加载中…</div>
|
||||
{:else}
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr><th>主体</th><th>级别</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if grants.length === 0}
|
||||
<tr><td colspan="3" class="quiet !py-[18px] text-center">暂无显式授权</td></tr>
|
||||
{:else}
|
||||
{#each grants as g (g.id)}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="flex text-ink-3"><Icon name={g.principalType === "USER" ? "user" : "group"} size={14} /></span>
|
||||
<span class="font-mono text-[12px]">{g.principalId}</span>
|
||||
{#if g.isCreatorGrant}<span class="quiet">(创建者)</span>{/if}
|
||||
</span>
|
||||
</td>
|
||||
<td class="file-meta">{g.role}</td>
|
||||
<td class="text-right">
|
||||
<!-- 创建者授权不可动(契约 8.1);非 MANAGE 也不给收回入口。 -->
|
||||
{#if !g.isCreatorGrant && canManage}
|
||||
<button class="link-danger inline-flex items-center gap-1" onclick={() => void revoke(g)}>
|
||||
<Icon name="minus" size={12} /> 收回
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
{#if canManage}
|
||||
<div class="my-3.5 border-t border-line-soft"></div>
|
||||
<div class="section-title mb-2.5">新增授权</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<select class="select !w-[110px]" bind:value={principalType} onchange={onTypeChange}>
|
||||
<option value="USER">用户</option>
|
||||
<option value="GROUP">Group</option>
|
||||
</select>
|
||||
|
||||
{#if principalType === "USER"}
|
||||
<input class="input min-w-0 flex-1" placeholder="用户 id" bind:value={userIdInput} />
|
||||
{:else if groupOptions === null}
|
||||
<span class="quiet flex-1">加载 Group 列表…</span>
|
||||
{:else if groupOptions.length === 0}
|
||||
<span class="quiet flex-1">暂无可选 Group · 先到「Group 管理」建一个</span>
|
||||
{:else}
|
||||
<select class="select min-w-0 flex-1" bind:value={groupId}>
|
||||
{#each groupOptions as g (g.id)}
|
||||
<option value={g.id}>{g.breadcrumb}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
|
||||
<select class="select !w-[110px]" bind:value={role}>
|
||||
{#each ROLES as r (r)}
|
||||
<option value={r}>{r}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button class="btn btn-primary disabled:opacity-50" onclick={addGrant} disabled={saving}>
|
||||
{saving ? "授予中…" : "授予"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-note mt-1.5">MANAGE 仅创建者可授;创建者授权不可动(契约 8.1)</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
@@ -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}
|
||||
|
||||
@@ -12,8 +12,25 @@
|
||||
let exportJob = $state<ExportJob | null>(null);
|
||||
|
||||
const canEdit = $derived(node.role === "MANAGE" || node.role === "EDIT");
|
||||
const canManage = $derived(node.role === "MANAGE");
|
||||
const roleLabel = $derived(node.role === "MANAGE" ? "可管理" : node.role === "EDIT" ? "可编辑" : "只读");
|
||||
|
||||
/** 独立权限开关(仅 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;
|
||||
@@ -67,7 +84,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-line-soft bg-panel p-5">
|
||||
<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">
|
||||
@@ -77,7 +94,7 @@
|
||||
<span class="italic text-ink-3">暂无简介</span>
|
||||
{/if}
|
||||
{#if canEdit}
|
||||
<button class="ml-2.5 rounded-lg border border-line bg-panel px-2.5 py-0.5 align-middle text-[11.5px] text-ink transition hover:bg-hover" onclick={openEditDesc}>编辑</button>
|
||||
<button class="btn ml-2.5 !px-2.5 !py-0.5 align-middle !text-[11.5px]" onclick={openEditDesc}>编辑</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,39 +102,54 @@
|
||||
<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).toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric" })}</b></div>
|
||||
<div>最近修改 <b class="font-semibold text-ink">{new Date(node.updatedAt).toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric" })}</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>
|
||||
|
||||
<div class="my-4 border-t border-line-soft"></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="mb-2 text-[12.5px] font-semibold">导出</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<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={submitExport}>开始导出</button>
|
||||
{#if exportJob}
|
||||
<span class="font-mono text-[11px] text-ink-3">
|
||||
{#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>
|
||||
<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="mb-3">
|
||||
<label class="mb-1 block text-[11.5px] text-ink-3" for="desc-draft">简要说明这个项目的内容</label>
|
||||
<textarea id="desc-draft" rows="5" class="w-full rounded-lg border border-line bg-panel px-3 py-2 text-[13px] leading-7 outline-none focus:border-accent" bind:value={descDraft} placeholder="例如:高中物理必修一第三章,表面张力相关内容……"></textarea>
|
||||
<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="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={() => (showEditDesc = 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={saveDesc}>保存</button>
|
||||
<button class="btn" onclick={() => (showEditDesc = false)}>取消</button>
|
||||
<button class="btn btn-primary" onclick={saveDesc}>保存</button>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
@@ -44,10 +44,22 @@ export async function registerFileLibRoutes(
|
||||
/* ------------------------------------------------------------ 身份 */
|
||||
|
||||
// 前端判断能力面用:是否网站管理员(root 创建按钮显隐)。
|
||||
// displayName/avatarUrl 供侧栏身份区显示 —— 页面不再服务端渲染(ADR-0029),
|
||||
// 旧 renderDashboard 在 handler 里查 Prisma 拿到的名字,现在必须由这里带出去,
|
||||
// 否则前端只有 userId 可显示。
|
||||
app.get("/database/api/me", async (request, reply) => {
|
||||
const actor = await actorOrNull(request, reply, deps);
|
||||
if (actor === null) return reply;
|
||||
return { userId: actor.userId, isWebsiteAdmin: actor.isWebsiteAdmin };
|
||||
const user = await deps.prisma.user.findUnique({
|
||||
where: { id: actor.userId },
|
||||
select: { displayName: true, avatarUrl: true },
|
||||
});
|
||||
return {
|
||||
userId: actor.userId,
|
||||
isWebsiteAdmin: actor.isWebsiteAdmin,
|
||||
displayName: user?.displayName ?? actor.userId,
|
||||
avatarUrl: user?.avatarUrl ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------ 树节点 */
|
||||
|
||||
Reference in New Issue
Block a user