chore: save snapshot before tag 0.3

This commit is contained in:
liaibo
2025-12-27 20:23:35 +08:00
parent 9bd5c9c403
commit c0b7b40eee
508 changed files with 2097 additions and 356976 deletions
+2 -1
View File
@@ -28,13 +28,14 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
let sidebarInitialData: SidebarInitialData | null = null;
if (activeWorkspaceId) {
const dataset = await fetchSidebarDataset(supabase, activeWorkspaceId);
const dataset = await fetchSidebarDataset(supabase, activeWorkspaceId);
documents = dataset.documents;
sidebarInitialData = {
activeWorkspaceId,
workspaces,
documents: dataset.documents,
trashedDocuments: dataset.trashedDocuments,
mediaAssets: dataset.mediaAssets,
};
}
@@ -0,0 +1,154 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
export const dynamic = "force-dynamic";
type Action = "copy" | "move" | "delete" | "rename";
interface BatchPayload {
action: Action;
assetIds: string[];
targetDocumentId?: string;
newName?: string;
}
const BUCKET = process.env.NEXT_PUBLIC_SUPABASE_MEDIA_BUCKET ?? "workspace";
export async function POST(request: Request) {
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const payload = (await request.json()) as BatchPayload;
if (!payload?.action || !payload.assetIds?.length) {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const { data: assets, error: fetchError } = await supabase
.from("media_assets")
.select("*")
.in("id", payload.assetIds);
if (fetchError) {
return NextResponse.json({ error: fetchError.message }, { status: 500 });
}
if (!assets?.length) {
return NextResponse.json({ error: "未找到附件" }, { status: 404 });
}
try {
switch (payload.action) {
case "delete": {
await Promise.all(
assets.map(async (asset) => {
if (asset.storage_path) {
await supabase.storage.from(asset.bucket || BUCKET).remove([asset.storage_path]);
}
}),
);
const { error } = await supabase.from("media_assets").delete().in("id", payload.assetIds);
if (error) throw error;
return NextResponse.json({ ok: true });
}
case "rename": {
if (payload.assetIds.length !== 1 || !payload.newName) {
return NextResponse.json({ error: "重命名需要单个文件与新名称" }, { status: 400 });
}
const asset = assets[0];
const ext = asset.file_name?.includes(".") ? `.${asset.file_name.split(".").pop()}` : "";
const newFileName = payload.newName.includes(".") || !ext ? payload.newName : `${payload.newName}${ext}`;
const targetPath = `${session.user.id}/${asset.workspace_id}/${asset.document_id}/assets/${newFileName}`;
if (asset.storage_path) {
const moveResult = await supabase.storage
.from(asset.bucket || BUCKET)
.move(asset.storage_path, targetPath);
if (moveResult.error) throw moveResult.error;
}
const { error } = await supabase
.from("media_assets")
.update({
file_name: newFileName,
storage_path: targetPath,
file_url: null,
thumbnail_url: null,
})
.eq("id", asset.id);
if (error) throw error;
return NextResponse.json({ ok: true });
}
case "copy":
case "move": {
if (!payload.targetDocumentId) {
return NextResponse.json({ error: "缺少目标页面" }, { status: 400 });
}
const { data: targetDoc, error: docErr } = await supabase
.from("documents")
.select("workspace_id")
.eq("id", payload.targetDocumentId)
.single();
if (docErr || !targetDoc) {
return NextResponse.json({ error: "目标页面不存在" }, { status: 404 });
}
const results = [];
for (const asset of assets) {
const fileName = asset.file_name ?? "附件";
const targetPath = `${session.user.id}/${targetDoc.workspace_id}/${payload.targetDocumentId}/assets/${fileName}`;
const sourcePath = asset.storage_path;
const bucket = asset.bucket || BUCKET;
if (!sourcePath) continue;
if (payload.action === "copy") {
const copyRes = await supabase.storage.from(bucket).copy(sourcePath, targetPath);
if (copyRes.error) throw copyRes.error;
const { data: inserted, error } = await supabase
.from("media_assets")
.insert({
workspace_id: targetDoc.workspace_id,
document_id: payload.targetDocumentId,
asset_type: asset.asset_type,
file_name: fileName,
file_size: asset.file_size,
mime_type: asset.mime_type,
bucket,
storage_path: targetPath,
created_by: session.user.id,
})
.select("*")
.single();
if (error) throw error;
results.push(inserted);
} else {
const moveRes = await supabase.storage.from(bucket).move(sourcePath, targetPath);
if (moveRes.error) throw moveRes.error;
const { data: updated, error } = await supabase
.from("media_assets")
.update({
workspace_id: targetDoc.workspace_id,
document_id: payload.targetDocumentId,
storage_path: targetPath,
file_name: fileName,
file_url: null,
thumbnail_url: null,
})
.eq("id", asset.id)
.select("*")
.single();
if (error) throw error;
results.push(updated);
}
}
return NextResponse.json({ items: results });
}
default:
return NextResponse.json({ error: "不支持的操作" }, { status: 400 });
}
} catch (err) {
const message = err instanceof Error ? err.message : "操作失败";
return NextResponse.json({ error: message }, { status: 500 });
}
}