chore: upgrade onlyoffice to 9.2.1

This commit is contained in:
liaibo
2025-12-26 07:52:40 +08:00
parent 8356734742
commit 9bd5c9c403
8 changed files with 364 additions and 12 deletions
@@ -0,0 +1,78 @@
import { NextResponse } from "next/server";
import supabaseAdmin from "@/lib/supabase/admin";
export const dynamic = "force-dynamic";
const parseStoragePath = (fileUrl: string) => {
try {
const url = new URL(fileUrl);
const segments = url.pathname.split("/").filter(Boolean);
const objectIdx = segments.findIndex((seg) => seg === "object");
if (objectIdx === -1 || objectIdx + 2 >= segments.length) return null;
// pattern 1: /storage/v1/object/public/<bucket>/<path...>
if (segments[objectIdx + 1] === "public") {
const bucket = segments[objectIdx + 2];
const path = segments.slice(objectIdx + 3).join("/");
return { bucket, path };
}
// pattern 2: /storage/v1/object/sign/<bucket>/<path...> (token in query)
if (segments[objectIdx + 1] === "sign") {
const bucket = segments[objectIdx + 2];
const path = segments.slice(objectIdx + 3).join("/");
return { bucket, path };
}
return null;
} catch {
return null;
}
};
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const fileUrl = searchParams.get("fileUrl");
const fileName = searchParams.get("fileName") ?? undefined;
const forOnlyOffice = searchParams.get("for") === "onlyoffice";
const hostOverride = process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE;
if (searchParams.get("debug") === "1") {
return NextResponse.json({
keyLen: (process.env.SUPABASE_SERVICE_ROLE_KEY || "").length,
url: process.env.SUPABASE_URL,
});
}
if (!fileUrl) {
return NextResponse.json({ error: "缺少 fileUrl" }, { status: 400 });
}
const parsed = parseStoragePath(fileUrl);
if (!parsed) {
return NextResponse.json({ error: "无法解析 Supabase 存储路径" }, { status: 400 });
}
const { bucket, path } = parsed;
const { data, error } = await supabaseAdmin.storage
.from(bucket)
.createSignedUrl(path, 60 * 60, { download: fileName });
if (error || !data?.signedUrl) {
return NextResponse.json({ error: error?.message ?? "生成签名 URL 失败" }, { status: 500 });
}
let signedUrl = data.signedUrl;
if (
forOnlyOffice &&
hostOverride &&
(signedUrl.includes("127.0.0.1") || signedUrl.includes("localhost"))
) {
try {
const url = new URL(signedUrl);
url.hostname = hostOverride;
signedUrl = url.toString();
} catch {
// ignore parse errors
}
}
return NextResponse.json({ signedUrl });
}
@@ -5,7 +5,7 @@ import { extname } from "path";
export const dynamic = "force-dynamic";
const MEDIA_BUCKET = process.env.NEXT_PUBLIC_SUPABASE_MEDIA_BUCKET ?? "media";
const DOC_BUCKET = process.env.NEXT_PUBLIC_SUPABASE_DOC_BUCKET ?? "documents";
const resolveAssetType = (mime: string): "image" | "video" | "audio" | "file" => {
if (mime.startsWith("image/")) return "image";
@@ -41,7 +41,7 @@ export async function POST(request: Request) {
const path = `${workspaceId}/${Date.now()}-${uniqueId}${extension}`;
const assetType = resolveAssetType(file.type || "");
const { error: uploadError } = await supabase.storage.from(MEDIA_BUCKET).upload(path, buffer, {
const { error: uploadError } = await supabase.storage.from(DOC_BUCKET).upload(path, buffer, {
contentType: file.type,
upsert: false,
});
@@ -50,17 +50,17 @@ export async function POST(request: Request) {
return NextResponse.json({ error: uploadError.message }, { status: 500 });
}
const {
data: { publicUrl },
} = supabase.storage.from(MEDIA_BUCKET).getPublicUrl(path);
// 为私有桶生成临时访问链接(7 天);前端可在需要时通过 /api/media/signed-url 刷新
const { data: signed } = await supabase.storage.from(DOC_BUCKET).createSignedUrl(path, 60 * 60 * 24 * 7);
const signedUrl = signed?.signedUrl ?? "";
const { data: asset, error } = await supabase
.from("media_assets")
.insert({
workspace_id: workspaceId,
document_id: documentId,
file_url: publicUrl,
thumbnail_url: publicUrl,
file_url: signedUrl,
thumbnail_url: signedUrl,
asset_type: assetType,
file_name: file.name,
file_size: file.size,
+124
View File
@@ -0,0 +1,124 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
type EditorMode = "view" | "edit";
const loadScript = (src: string) =>
new Promise<void>((resolve, reject) => {
const existing = document.querySelector(`script[src="${src}"]`);
if (existing) {
existing.addEventListener("load", () => resolve(), { once: true });
resolve();
return;
}
const script = document.createElement("script");
script.src = src;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`));
document.body.appendChild(script);
});
const hashKey = (input: string) => {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {
hash = (hash << 5) - hash + input.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash).toString();
};
const docTypeFromExt = (ext: string) => {
const word = ["doc", "docx", "odt", "rtf"];
const slide = ["ppt", "pptx", "odp"];
const sheet = ["xls", "xlsx", "ods", "csv"];
if (word.includes(ext)) return "text";
if (slide.includes(ext)) return "presentation";
if (sheet.includes(ext)) return "spreadsheet";
return "text";
};
export default function OnlyOfficePage() {
const params = useSearchParams();
const fileUrl = params.get("fileUrl") ?? "";
const fileName = params.get("fileName") ?? "未命名文档";
const fileType = (params.get("fileType") ?? "docx").toLowerCase();
const mode = (params.get("mode") ?? "edit") as EditorMode;
const [error, setError] = useState<string | null>(null);
const baseUrl = process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL;
const storageHostOverride =
process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE;
const targetDocType = useMemo(() => docTypeFromExt(fileType), [fileType]);
const resolvedFileUrl = useMemo(() => {
if (!fileUrl) return "";
try {
const u = new URL(fileUrl);
if (
storageHostOverride &&
(u.hostname === "127.0.0.1" ||
u.hostname === "localhost" ||
u.hostname === "host.docker.internal")
) {
u.hostname = storageHostOverride;
}
return u.toString();
} catch {
return fileUrl;
}
}, [fileUrl, storageHostOverride]);
useEffect(() => {
if (!baseUrl) {
setError("缺少 NEXT_PUBLIC_ONLYOFFICE_BASE_URL 配置,无法加载编辑器。");
return;
}
if (!fileUrl) {
setError("缺少 fileUrl 参数。");
return;
}
const scriptUrl = `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api.js`;
loadScript(scriptUrl)
.then(() => {
// @ts-expect-error ONLYOFFICE 全局对象
if (!window.DocsAPI) {
throw new Error("未检测到 DocsAPI,请检查 ONLYOFFICE 版本。");
}
// eslint-disable-next-line new-cap,@typescript-eslint/no-explicit-any
new (window as any).DocsAPI.DocEditor("onlyoffice-frame", {
width: "100%",
height: "100%",
document: {
fileType,
title: fileName,
url: resolvedFileUrl,
key: hashKey(`${resolvedFileUrl}-${fileName}`),
},
documentType: targetDocType,
editorConfig: {
mode: mode === "view" ? "view" : "edit",
lang: "zh-CN",
customization: {
feedback: { visible: false },
},
},
});
})
.catch((err: Error) => {
setError(err.message);
});
}, [baseUrl, fileName, fileType, mode, resolvedFileUrl, targetDocType]);
if (error) {
return (
<div className="flex h-screen flex-col items-center justify-center gap-3 bg-slate-50">
<p className="text-base font-semibold text-red-600">ONLYOFFICE </p>
<p className="text-sm text-gray-600">{error}</p>
</div>
);
}
return <div id="onlyoffice-frame" className="h-screen w-screen bg-slate-50" />;
}