forked from bai/curriculum-project-hub
d072e9ec1e
- 回收站: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 例绿
68 lines
2.3 KiB
Svelte
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>
|