Files
curriculum-project-hub/hub/filelib-web/src/lib/RecentView.svelte
T
ymy d072e9ec1e feat(filelib): 老师端左栏导航:回收站 + 最近打开(ADR-0031)
- 回收站:listBin(祖先全活跃的已删顶点;管理员/直连 MANAGE 可见)、
  restore(与 D15 对称只清本节点,落审计)、purge(仅管理员,pathIds 枚举
  子树按深度降序分批硬删,绕过 self-FK RESTRICT)
- 最近打开:FileLibRecentVisit 表(filePath='' 兜底 PG 唯一索引),客户端
  成功打开后上报(VIEW 门禁,upsert 刷新),列表 20 条,D8/D15 可见性过滤
- 前端:/app 左栏(文件库/最近打开/回收站);RecentView/BinView;
  GridLibraryView 埋点 + navTarget 跳转(breadcrumb 建栈,role 已捎带)
- 测试:filelib-nav 集成 4 例;全套 79 例绿
2026-07-31 13:27:04 +08:00

68 lines
2.3 KiB
Svelte

<script lang="ts">
/**
* 最近打开(ADR-0031):本人最近 20 条,点击跳回文件库对应位置
* (经 onopen 把导航目标交给外层,由 GridLibraryView 建栈跳转)。
*/
import { onMount } from "svelte";
import { api } from "./api.js";
import type { RecentEntry } from "./types.js";
import Icon from "./Icon.svelte";
let { onopen }: { onopen: (target: { nodeId: string; filePath?: string }) => void } = $props();
let entries = $state<RecentEntry[] | null>(null);
let error = $state<string | null>(null);
onMount(async () => {
try {
const r = await api<{ entries: RecentEntry[] }>("/database/api/recent");
entries = r.entries;
} catch (e) {
error = e instanceof Error ? e.message : String(e);
}
});
function fmt(iso: string): string {
try {
return new Date(iso).toLocaleString("zh-CN", { dateStyle: "medium", timeStyle: "short" });
} catch {
return iso;
}
}
function iconOf(e: RecentEntry): "folder" | "layers" | "chevron" {
if (e.filePath !== "") return "chevron";
return e.kind === "FOLDER" ? "folder" : "layers";
}
</script>
<div class="flex-1 overflow-y-auto px-6 py-5">
<h1 class="mb-4 text-[15px] font-semibold text-ink">最近打开</h1>
{#if error !== null}
<div class="py-8 text-center text-[13px] text-danger">{error}</div>
{:else if entries === null}
<div class="quiet py-8 text-center">加载中…</div>
{:else if entries.length === 0}
<div class="quiet py-8 text-center">还没有访问记录 · 去文件库逛逛</div>
{:else}
<div class="panel !p-2">
{#each entries as e (e.nodeId + "/" + e.filePath)}
<button
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left transition hover:bg-hover"
onclick={() => onopen({ nodeId: e.nodeId, ...(e.filePath !== "" ? { filePath: e.filePath } : {}) })}
>
<span class="flex text-ink-3"><Icon name={iconOf(e)} size={15} /></span>
<span class="min-w-0 flex-1">
<span class="block truncate text-[13px] text-ink">{e.name}</span>
{#if e.filePath !== ""}
<span class="block truncate font-mono text-[11px] text-ink-3">{e.filePath}</span>
{/if}
</span>
<span class="quiet shrink-0">{fmt(e.openedAt)}</span>
</button>
{/each}
</div>
{/if}
</div>