forked from bai/curriculum-project-hub
feat(filelib): 移除最近打开模块(ADR-0032,supersede ADR-0031 对应半部)
- FileLibRecentVisit 删表(手写迁移;表当日新建无生产数据) - recentService/recentRoutes/RecentView 删除;GridLibraryView 埋点与 navTarget 跳转一并移除;types 清 RecentEntry - 左栏保留 文件库/回收站(ADR-0031 回收站半部不受影响) - breadcrumb 的 role 字段保留(独立可用的增量字段)
This commit is contained in:
@@ -23,14 +23,6 @@
|
||||
import GrantsPanel from "./GrantsPanel.svelte";
|
||||
import OverviewPanel from "./OverviewPanel.svelte";
|
||||
|
||||
/** 最近打开上报的导航目标(ADR-0031):父组件传入后,本组件跳到对应节点并清除。 */
|
||||
export interface NavTarget {
|
||||
readonly nodeId: string;
|
||||
readonly filePath?: string | undefined;
|
||||
}
|
||||
|
||||
let { navTarget = null, onnavigated }: { navTarget?: NavTarget | null; onnavigated?: () => void } = $props();
|
||||
|
||||
const RANK: Record<Role, number> = { VIEW: 1, EDIT: 2, MANAGE: 3 };
|
||||
const atLeast = (role: Role, min: Role): boolean => RANK[role] >= RANK[min];
|
||||
|
||||
@@ -107,14 +99,6 @@
|
||||
|
||||
onMount(loadChildren);
|
||||
|
||||
/** 最近打开上报(ADR-0031):fire-and-forget,失败静默,不阻塞浏览。 */
|
||||
function record(nodeId: string, filePath?: string): void {
|
||||
void api("/database/api/recent", {
|
||||
method: "POST",
|
||||
body: { nodeId, ...(filePath !== undefined ? { filePath } : {}) },
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
|
||||
function refresh(): void {
|
||||
selected = null;
|
||||
menu = null;
|
||||
@@ -126,7 +110,6 @@
|
||||
|
||||
function openNode(n: StackItem): void {
|
||||
selected = null;
|
||||
record(n.id);
|
||||
if (n.kind === "FOLDER") {
|
||||
stack = [...stack, n];
|
||||
void loadChildren();
|
||||
@@ -174,43 +157,6 @@
|
||||
void loadChildren();
|
||||
}
|
||||
|
||||
/** 跳到任意节点(最近打开入口):breadcrumb 建栈,FOLDER 进子层,PROJECT 进文件视图。 */
|
||||
async function navigateTo(target: NavTarget): Promise<void> {
|
||||
try {
|
||||
const r = await api<{ breadcrumb: Array<{ id: string | null; name: string | null; kind: "FOLDER" | "PROJECT"; role: Role | null }> }>(
|
||||
`/database/api/nodes/${target.nodeId}/breadcrumb`,
|
||||
);
|
||||
const visible = r.breadcrumb.filter(
|
||||
(e): e is { id: string; name: string; kind: "FOLDER" | "PROJECT"; role: Role | null } =>
|
||||
e.id !== null && e.name !== null,
|
||||
);
|
||||
if (visible.length === 0) return;
|
||||
const self = visible[visible.length - 1]!;
|
||||
selected = null;
|
||||
if (self.kind === "FOLDER") {
|
||||
view = "nodes";
|
||||
projectNode = null;
|
||||
clearSelectedFile();
|
||||
stack = visible.map((e) => ({ id: e.id, name: e.name, kind: e.kind, role: e.role ?? "VIEW" }));
|
||||
await loadChildren();
|
||||
} else {
|
||||
stack = visible.slice(0, -1).map((e) => ({ id: e.id, name: e.name, kind: e.kind, role: e.role ?? "VIEW" }));
|
||||
projectNode = await fetchDetail(self.id);
|
||||
view = "files";
|
||||
await loadFiles();
|
||||
if (target.filePath !== undefined) selectedFilePath.set(target.filePath);
|
||||
}
|
||||
} catch (e) {
|
||||
toastErr(errText(e));
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (navTarget === null) return;
|
||||
const t = navTarget;
|
||||
void navigateTo(t).finally(() => onnavigated?.());
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------ 节点操作 */
|
||||
|
||||
function openCreate(kind: "FOLDER" | "PROJECT", parentId: string | null): void {
|
||||
@@ -300,7 +246,6 @@
|
||||
/* ------------------------------------------------------------ 文件操作 */
|
||||
|
||||
function previewFile(f: FileEntry): void {
|
||||
if (projectNode !== null) record(projectNode.id, f.path);
|
||||
selectedFilePath.set(f.path);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<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>
|
||||
@@ -132,16 +132,6 @@ export interface UserSearchResult {
|
||||
readonly avatarUrl: string | null;
|
||||
}
|
||||
|
||||
/** 最近打开条目(GET /database/api/recent)。 */
|
||||
export interface RecentEntry {
|
||||
readonly nodeId: string;
|
||||
readonly kind: NodeKind;
|
||||
readonly name: string;
|
||||
/** "" = 节点本身;非空 = 项目内文件路径。 */
|
||||
readonly filePath: string;
|
||||
readonly openedAt: string;
|
||||
}
|
||||
|
||||
/** 回收站条目(GET /database/api/bin)。 */
|
||||
export interface BinEntry {
|
||||
readonly id: string;
|
||||
|
||||
@@ -4,25 +4,17 @@
|
||||
import { me, authChecked } from "$lib/stores.js";
|
||||
import { loadSession } from "$lib/session.js";
|
||||
import LoginView from "$lib/LoginView.svelte";
|
||||
import GridLibraryView, { type NavTarget } from "$lib/GridLibraryView.svelte";
|
||||
import RecentView from "$lib/RecentView.svelte";
|
||||
import GridLibraryView from "$lib/GridLibraryView.svelte";
|
||||
import BinView from "$lib/BinView.svelte";
|
||||
import Icon from "$lib/Icon.svelte";
|
||||
|
||||
onMount(loadSession);
|
||||
|
||||
type View = "library" | "recent" | "bin";
|
||||
type View = "library" | "bin";
|
||||
let view = $state<View>("library");
|
||||
let navTarget = $state<NavTarget | null>(null);
|
||||
|
||||
function openFromRecent(target: NavTarget): void {
|
||||
navTarget = target;
|
||||
view = "library";
|
||||
}
|
||||
|
||||
const tabs: ReadonlyArray<readonly [View, string, "layers" | "clock" | "trash"]> = [
|
||||
const tabs: ReadonlyArray<readonly [View, string, "layers" | "trash"]> = [
|
||||
["library", "文件库", "layers"],
|
||||
["recent", "最近打开", "clock"],
|
||||
["bin", "回收站", "trash"],
|
||||
];
|
||||
</script>
|
||||
@@ -49,9 +41,7 @@
|
||||
</nav>
|
||||
|
||||
{#if view === "library"}
|
||||
<GridLibraryView {navTarget} onnavigated={() => (navTarget = null)} />
|
||||
{:else if view === "recent"}
|
||||
<RecentView onopen={openFromRecent} />
|
||||
<GridLibraryView />
|
||||
{:else}
|
||||
<BinView />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user