forked from bai/curriculum-project-hub
feat(filelib-web): 把 /database 各页从后端 HTML 拼接迁到 SvelteKit 路由
登录页、后台外壳与六个 tab 全部成为客户端路由: /database/admin 登录(迁自 renderLoginPage) /database/dashboard 概览(迁自 renderDashboard) .../library .../users .../groups .../search .../settings 六个 tab 是真 URL,不再是 location.hash + display:none —— 刷新不丢 位置,链接可分享。 BrowserShell 拆成 LibraryView,加 showUserFooter:老师端 /app 显示 身份/登出页脚,后台的文件库 tab 不显示(外层已有身份区)。 bootstrap 走 /database/config 而非 /database/api/login-info:后者由 teacherApp 在 silo org 查找成功后才注册,前者无条件注册,登录页在 org 未就绪时也必须能拿到配置。 后端拥有的链接(OAuth、DEV 一键登录)标 data-sveltekit-reload, 否则被客户端路由拦下。
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* 管理后台外壳(迁自后端 renderDashboard 的侧栏 + 身份区)。
|
||||
*
|
||||
* 与旧实现的区别:六个 tab 是真 URL 路由(/database/dashboard/library 等),
|
||||
* 不再是 location.hash + display:none 切换 —— 刷新不丢位置,链接可分享。
|
||||
*
|
||||
* 权限门:未登录跳 /database/admin;登录但非 OWNER/ADMIN(isWebsiteAdmin)
|
||||
* 显示无权提示。语义与 ADR-0028 一致 —— 管理面板要求 silo org 的 OWNER/ADMIN。
|
||||
*/
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import { me, authChecked } from "$lib/stores.js";
|
||||
import { loadSession, logout } from "$lib/session.js";
|
||||
import Avatar from "$lib/Avatar.svelte";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const NAV = [
|
||||
{ seg: "", label: "概览", icon: "M4 13h6V4H4v9Zm0 7h6v-5H4v5Zm10 0h6V11h-6v9Zm0-16v5h6V4h-6Z" },
|
||||
{ seg: "library", label: "文件库", icon: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" },
|
||||
{ seg: "users", label: "用户管理", icon: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm13 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" },
|
||||
{ seg: "groups", label: "Group 管理", icon: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm14 10v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75M23 21v-2a4 4 0 0 0-3-3.87" },
|
||||
{ seg: "search", label: "查询", icon: "m21 21-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z" },
|
||||
{ seg: "settings", label: "设置", icon: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm7-3 2 1-2 3-2-1a7 7 0 0 1-2 1l-1 2h-4l-1-2a7 7 0 0 1-2-1l-2 1-2-3 2-1a7 7 0 0 1 0-2l-2-1 2-3 2 1a7 7 0 0 1 2 1l1-2h4l1 2a7 7 0 0 1 0 2l2-1 2 3-2 1a7 7 0 0 1 0 2Z" },
|
||||
] as const;
|
||||
|
||||
const BASE = "/database/dashboard";
|
||||
|
||||
onMount(async () => {
|
||||
await loadSession();
|
||||
if ($me === null) void goto("/database/admin", { replaceState: true });
|
||||
});
|
||||
|
||||
function href(seg: string): string {
|
||||
return seg === "" ? BASE : `${BASE}/${seg}`;
|
||||
}
|
||||
|
||||
function isActive(seg: string): boolean {
|
||||
const path = page.url.pathname.replace(/\/$/, "");
|
||||
return seg === "" ? path === BASE : path === `${BASE}/${seg}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Database Admin</title></svelte:head>
|
||||
|
||||
{#if !$authChecked}
|
||||
<div class="flex h-full items-center justify-center text-[13px] text-ink-3">加载中…</div>
|
||||
{:else if $me === null}
|
||||
<div class="flex h-full items-center justify-center text-[13px] text-ink-3">跳转到登录页…</div>
|
||||
{:else if !$me.isWebsiteAdmin}
|
||||
<div class="flex min-h-full items-center justify-center p-6">
|
||||
<div class="w-full max-w-[420px] rounded-2xl border border-line-soft bg-panel p-9 text-center shadow-[0_4px_20px_rgba(26,26,24,.07)]">
|
||||
<h2 class="mb-2 text-lg font-semibold text-ink">无权访问管理后台</h2>
|
||||
<p class="mb-6 text-[13px] text-ink-3">
|
||||
当前账号不是本组织的所有者或管理员。普通老师请到文件库使用。
|
||||
</p>
|
||||
<a href="/app" class="btn btn-primary justify-center">前往文件库</a>
|
||||
<button class="btn mt-3 w-full justify-center" onclick={logout}>退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full">
|
||||
<aside class="flex w-[240px] shrink-0 flex-col border-r border-line-soft bg-sidebar">
|
||||
<div class="border-b border-line-soft px-4 py-4">
|
||||
<span class="text-[15px] font-semibold text-ink">Database Admin</span>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-1 flex-col gap-0.5 p-2.5">
|
||||
{#each NAV as item (item.seg)}
|
||||
{@const active = isActive(item.seg)}
|
||||
<a
|
||||
href={href(item.seg)}
|
||||
class="flex items-center gap-2.5 rounded-[10px] px-3.5 py-2 text-[13px] transition"
|
||||
class:bg-selected={active}
|
||||
class:text-ink={active}
|
||||
class:font-semibold={active}
|
||||
class:text-ink-3={!active}
|
||||
class:hover:bg-hover={!active}
|
||||
>
|
||||
<svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={item.icon} /></svg>
|
||||
{item.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="m-2.5 flex items-center gap-2.5 border-t border-line-soft px-3 py-2.5">
|
||||
<Avatar displayName={$me.displayName} userId={$me.userId} avatarUrl={$me.avatarUrl} size={26} />
|
||||
<p class="min-w-0 flex-1 truncate text-[12.5px] text-ink" title={$me.userId}>{$me.displayName}</p>
|
||||
<button class="btn !px-2.5 !py-[3px] !text-[11px]" onclick={logout}>退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user