forked from bai/curriculum-project-hub
3d0f4e5c2d
登录页、后台外壳与六个 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, 否则被客户端路由拦下。
68 lines
2.3 KiB
Svelte
68 lines
2.3 KiB
Svelte
<script lang="ts">
|
|
/** 概览(迁自后端 renderDashboard 的统计卡片 + 最近活动)。数据走 GET /database/api/stats。 */
|
|
import { onMount } from "svelte";
|
|
import { api } from "$lib/api.js";
|
|
import type { DashboardStats } from "$lib/types.js";
|
|
|
|
let stats = $state<DashboardStats | null>(null);
|
|
let error = $state<string | null>(null);
|
|
|
|
onMount(async () => {
|
|
try {
|
|
stats = await api<DashboardStats>("/database/api/stats");
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : String(e);
|
|
}
|
|
});
|
|
|
|
const cards = $derived([
|
|
{ label: "文件夹", value: stats?.folders },
|
|
{ label: "项目", value: stats?.projects },
|
|
{ label: "文件", value: stats?.files },
|
|
{ label: "活跃授权", value: stats?.grants },
|
|
]);
|
|
|
|
function fmtWhen(iso: string): string {
|
|
try {
|
|
return new Date(iso).toLocaleString("zh-CN");
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<section class="flex-1 overflow-y-auto p-7">
|
|
<h1 class="mb-1 text-lg font-semibold text-ink">概览</h1>
|
|
<p class="mb-5 text-[11.5px] text-ink-3">文件库实时数据</p>
|
|
|
|
{#if error}
|
|
<div class="panel text-[12.5px] text-danger">{error}</div>
|
|
{:else}
|
|
<div class="grid grid-cols-4 gap-4">
|
|
{#each cards as card (card.label)}
|
|
<div class="panel !px-5 !py-[18px]">
|
|
<p class="text-[12.5px] text-ink-3">{card.label}</p>
|
|
<p class="mt-1.5 text-[28px] font-semibold text-ink">{card.value ?? "—"}</p>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="panel mt-[18px]">
|
|
<h2 class="mb-2 text-[13.5px] font-semibold text-ink">最近活动</h2>
|
|
{#if stats === null}
|
|
<div class="quiet py-6 text-center">加载中…</div>
|
|
{:else if stats.recent.length === 0}
|
|
<div class="quiet py-[26px] text-center">暂无文件库活动 · 到「文件库」里创建第一个文件夹吧</div>
|
|
{:else}
|
|
{#each stats.recent as row (row.action + row.when + row.label)}
|
|
<div class="flex items-center gap-3 border-t border-line-soft py-2.5 text-[13px]">
|
|
<span class="tag shrink-0">{row.action}</span>
|
|
<span class="truncate text-ink">{row.label}</span>
|
|
<span class="ml-auto shrink-0 text-[11.5px] text-ink-3">{row.actor} · {fmtWhen(row.when)}</span>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</section>
|