forked from EduCraft/curriculum-project-hub
beaa92de2e
能删进回收站(MANAGE)的人就能清空;无关者 404(D8)。二次确认与 node.purge 审计不变;BinView 彻底删除按钮对全部可见条目开放。
110 lines
3.7 KiB
Svelte
110 lines
3.7 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* 回收站(ADR-0031):祖先全活跃的已删节点顶;恢复只清本节点 deletedAt;
|
|
* 彻底删除(整支硬删)仅网站管理员,二次确认。
|
|
*/
|
|
import { onMount } from "svelte";
|
|
import { api } from "./api.js";
|
|
import { toastErr, toastOk } from "./stores.js";
|
|
import type { BinEntry } from "./types.js";
|
|
import Icon from "./Icon.svelte";
|
|
|
|
let entries = $state<BinEntry[] | null>(null);
|
|
let error = $state<string | null>(null);
|
|
let busyId = $state<string | null>(null);
|
|
|
|
async function load(): Promise<void> {
|
|
try {
|
|
const r = await api<{ entries: BinEntry[] }>("/database/api/bin");
|
|
entries = r.entries;
|
|
error = null;
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : String(e);
|
|
}
|
|
}
|
|
|
|
onMount(load);
|
|
|
|
function fmt(iso: string): string {
|
|
try {
|
|
return new Date(iso).toLocaleString("zh-CN", { dateStyle: "medium", timeStyle: "short" });
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
async function restore(e: BinEntry): Promise<void> {
|
|
busyId = e.id;
|
|
try {
|
|
const r = await api<{ name: string; renamedFrom?: string }>(
|
|
`/database/api/bin/${encodeURIComponent(e.id)}/restore`,
|
|
{ method: "POST" },
|
|
);
|
|
toastOk(
|
|
r.renamedFrom !== undefined && r.renamedFrom !== r.name
|
|
? `已恢复为「${r.name}」(原名与现有节点冲突)`
|
|
: `已恢复「${r.name}」`,
|
|
);
|
|
await load();
|
|
} catch (err) {
|
|
toastErr(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
busyId = null;
|
|
}
|
|
}
|
|
|
|
async function purge(e: BinEntry): Promise<void> {
|
|
if (!confirm(`彻底删除「${e.name}」及其全部内容?此操作不可恢复。`)) return;
|
|
if (!confirm(`再次确认:整支硬删,含子文件夹/项目/文件与授权记录。继续?`)) return;
|
|
busyId = e.id;
|
|
try {
|
|
const r = await api<{ removed: number }>(`/database/api/bin/${encodeURIComponent(e.id)}`, { method: "DELETE" });
|
|
toastOk(`已彻底删除(${r.removed} 个节点)`);
|
|
await load();
|
|
} catch (err) {
|
|
toastErr(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
busyId = null;
|
|
}
|
|
}
|
|
</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.id)}
|
|
<div class="flex items-center gap-3 rounded-lg px-3 py-2">
|
|
<span class="flex text-ink-3"><Icon name={e.kind === "FOLDER" ? "folder" : "layers"} size={15} /></span>
|
|
<span class="min-w-0 flex-1">
|
|
<span class="block truncate text-[13px] text-ink">{e.name}</span>
|
|
<span class="quiet block">删除于 {fmt(e.deletedAt)}</span>
|
|
</span>
|
|
<button
|
|
class="btn btn-sm disabled:opacity-50"
|
|
onclick={() => void restore(e)}
|
|
disabled={busyId === e.id}
|
|
>
|
|
<Icon name="restore" size={12} /> 恢复
|
|
</button>
|
|
<!-- 彻底删除与条目可见性同权(ADR-0034):能在回收站看到,就能清空 -->
|
|
<button
|
|
class="btn btn-sm btn-danger disabled:opacity-50"
|
|
onclick={() => void purge(e)}
|
|
disabled={busyId === e.id}
|
|
>
|
|
<Icon name="trash" size={12} /> 彻底删除
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|