0.2 在线版本打通

This commit is contained in:
liaibo
2026-01-15 20:54:21 +08:00
parent 725a60d3aa
commit 94957dc361
1596 changed files with 153254 additions and 309 deletions
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import { useSessionContext } from "@supabase/auth-helpers-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
interface TaskResponse {
task_id: string;
@@ -20,7 +21,7 @@ export function DocumentTaskPanel({ documentId }: Props) {
const { session } = useSessionContext();
const [task, setTask] = useState<TaskResponse | null>(null);
const [pending, setPending] = useState(false);
const backendUrl = useMemo(() => process.env.NEXT_PUBLIC_BACKEND_URL, []);
const backendUrl = useMemo(() => getMnoteRuntimeConfig().backendUrl, []);
const triggerTask = async () => {
if (!backendUrl || !session?.access_token) return;
@@ -17,6 +17,7 @@ import { cn } from "@/lib/utils";
import { useImagePicker } from "@/components/media/image-picker-context";
import type { MediaKind } from "@/types/media";
import { emitAssetsChanged } from "@/lib/events";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import {
DropdownMenu,
DropdownMenuContent,
@@ -97,7 +98,7 @@ const MediaBlockContent = ({ block, editor }: any) => {
const captionRef = useRef<HTMLInputElement | null>(null);
const [captionEditing, setCaptionEditing] = useState(false);
const shouldShowCaption = captionEditing || Boolean(block.props.caption);
const officeBase = process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL;
const officeBase = getMnoteRuntimeConfig().onlyofficeBaseUrl;
const resolveDocumentId = useCallback(() => {
if (typeof window !== "undefined") {
const [, tail] = window.location.pathname.split("/documents/");
@@ -240,11 +241,14 @@ const MediaBlockContent = ({ block, editor }: any) => {
return;
}
try {
const res = await fetch(
`/api/media/signed-url?fileUrl=${encodeURIComponent(fileUrl)}&fileName=${encodeURIComponent(
displayFileName,
)}&for=onlyoffice`,
);
const assetId = (block.props as { assetId?: string })?.assetId;
const res = assetId
? await fetch(`/api/media/sign?assetId=${encodeURIComponent(assetId)}`)
: await fetch(
`/api/media/signed-url?fileUrl=${encodeURIComponent(fileUrl)}&fileName=${encodeURIComponent(
displayFileName,
)}&for=onlyoffice`,
);
if (!res.ok) {
const payload = await res.json().catch(() => null);
throw new Error(payload?.error ?? "生成签名链接失败");
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { createReactBlockSpec } from "@blocknote/react";
import { RiFileTextFill } from "react-icons/ri";
import { useRouter } from "next/navigation";
import { supabaseBrowser } from "@/lib/supabase/client";
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
const normalizeTitle = (value?: string | null) => {
if (!value || !value.trim()) {
@@ -15,6 +15,7 @@ const normalizeTitle = (value?: string | null) => {
const PageReferenceContent = ({ pageId, title }: { pageId: string; title: string }) => {
const router = useRouter();
const supabaseBrowser = getSupabaseBrowserClient();
const fallbackTitle = normalizeTitle(title);
const [resolvedTitle, setResolvedTitle] = useState(fallbackTitle);
@@ -39,7 +40,7 @@ const PageReferenceContent = ({ pageId, title }: { pageId: string; title: string
.from("documents")
.select("title")
.eq("id", pageId)
.single();
.single<{ title: string | null }>();
if (data) {
applyTitle(data.title);
}
@@ -14,7 +14,7 @@ import {
import { useLuckysheetLoader } from "@/components/online-table/useLuckysheetLoader";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { extractRowsForPreview } from "@/components/online-table/utils";
import { supabaseBrowser } from "@/lib/supabase/client";
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
type LuckysheetSelection =
| {
@@ -52,6 +52,7 @@ const VIEWER_CONTAINER_PREFIX = "headless-table-viewer-";
const HeadlessTableViewer: React.FC<HeadlessTableViewerProps> = ({ tableId, embed = false, editable }) => {
const containerId = useMemo(() => `${VIEWER_CONTAINER_PREFIX}${tableId}`, [tableId]);
const supabaseBrowser = useMemo(() => getSupabaseBrowserClient(), []);
const containerRef = useRef<HTMLDivElement>(null);
const isLuckysheetReady = useLuckysheetLoader();
const [table, setTable] = useState<DocumentTable | null>(null);
@@ -1,7 +1,6 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState, type ReactNode } from "react";
interface QueryProviderProps {
@@ -25,7 +24,6 @@ export function QueryProvider({ children }: QueryProviderProps) {
return (
<QueryClientProvider client={client}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
@@ -3,7 +3,7 @@
import { useEffect } from "react";
import type { Session } from "@supabase/supabase-js";
import { SessionContextProvider } from "@supabase/auth-helpers-react";
import { supabaseBrowser } from "@/lib/supabase/client";
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
interface SupabaseProviderProps {
session: Session | null;
@@ -12,6 +12,7 @@ interface SupabaseProviderProps {
export function SupabaseProvider({ session, children }: SupabaseProviderProps) {
useEffect(() => {
const supabaseBrowser = getSupabaseBrowserClient();
const {
data: { subscription },
} = supabaseBrowser.auth.onAuthStateChange((_event, newSession) => {
@@ -40,7 +41,10 @@ export function SupabaseProvider({ session, children }: SupabaseProviderProps) {
}, []);
return (
<SessionContextProvider supabaseClient={supabaseBrowser} initialSession={session}>
<SessionContextProvider
supabaseClient={getSupabaseBrowserClient()}
initialSession={session}
>
{children}
</SessionContextProvider>
);
@@ -37,7 +37,7 @@ import type { SidebarInitialData, SidebarSectionId } from "@/components/sidebar/
import { useSidebarData } from "@/hooks/use-sidebar-data";
import { PrivateTree } from "@/components/sidebar/private-tree";
import { buildSidebarSectionsFromTree, flattenDocumentTree } from "@/lib/sidebar-tree";
import { supabaseBrowser } from "@/lib/supabase/client";
import { getSupabaseBrowserClient } from "@/lib/supabase/client";
import { useSearchPaletteStore } from "@/store/search-palette";
import { useEditorBridgeStore } from "@/store/editor-bridge";
import { FileTree } from "@/components/sidebar/file-tree";
@@ -57,6 +57,7 @@ import {
import type { MediaAsset } from "@/types/media";
import { AssetContextMenu } from "@/components/sidebar/asset-context-menu";
import { ASSETS_CHANGED_EVENT, DOCUMENTS_CHANGED_EVENT, emitAssetsChanged, emitDocumentsChanged } from "@/lib/events";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
const TOP_BUTTONS = [
{ id: "search", icon: SearchIcon, label: "搜索" },
@@ -78,6 +79,21 @@ const SECTION_ICONS: Record<Exclude<SidebarSectionId, "private">, React.ReactNod
const normalizeStoragePath = (storagePath: string): string => storagePath.replaceAll("\\", "/");
const officeFileTypeFromAsset = (fileName: string | null, mimeType: string | null) => {
const name = (fileName ?? "").trim().toLowerCase();
const mt = (mimeType ?? "").trim().toLowerCase();
const ext = name.includes(".") ? name.split(".").pop() : null;
if (ext && ["doc", "docx", "odt", "rtf"].includes(ext)) return ext;
if (ext && ["ppt", "pptx", "odp"].includes(ext)) return ext;
if (ext && ["xls", "xlsx", "ods", "csv"].includes(ext)) return ext;
if (ext && ["pdf"].includes(ext)) return ext;
if (mt.includes("wordprocessingml")) return "docx";
if (mt.includes("presentationml")) return "pptx";
if (mt.includes("spreadsheetml")) return "xlsx";
if (mt.includes("pdf")) return "pdf";
return null;
};
const extractMindmapIdFromStoragePath = (
storagePath: string | null | undefined,
): string | null => {
@@ -112,6 +128,7 @@ interface ContextMenuState {
export function Sidebar({ initialData }: SidebarProps) {
const { open, setOpen, width, setWidth, collapsedSections, toggleSection, setSectionCollapsed, trashConfirm, setTrashConfirm } =
useSidebarStore();
const supabaseBrowser = useMemo(() => getSupabaseBrowserClient(), []);
const sectionsTrayOpen = useSidebarStore((state) => state.sectionsTrayOpen);
const toggleSectionsTray = useSidebarStore((state) => state.toggleSectionsTray);
const setSectionsTrayOpen = useSidebarStore((state) => state.setSectionsTrayOpen);
@@ -530,6 +547,36 @@ export function Sidebar({ initialData }: SidebarProps) {
setOpen(false);
return;
}
const officeFileType = officeFileTypeFromAsset(asset.file_name ?? null, asset.mime_type ?? null);
if (officeFileType) {
const officeBase = getMnoteRuntimeConfig().onlyofficeBaseUrl;
if (!officeBase) {
window.alert("缺少 ONLYOFFICE 地址,请在 .env.local 配置 NEXT_PUBLIC_ONLYOFFICE_BASE_URL");
return;
}
void (async () => {
try {
const res = await fetch(`/api/media/sign?assetId=${encodeURIComponent(asset.id)}`);
if (!res.ok) {
const payload = await res.json().catch(() => null);
throw new Error(payload?.error ?? "生成签名链接失败");
}
const { signedUrl } = (await res.json()) as { signedUrl: string };
const target = new URL("/onlyoffice", window.location.origin);
target.searchParams.set("fileUrl", signedUrl);
target.searchParams.set("fileName", asset.file_name ?? `未命名.${officeFileType}`);
target.searchParams.set("fileType", officeFileType);
target.searchParams.set("assetId", asset.id);
window.open(target.toString(), "_blank", "noopener,noreferrer");
setOpen(false);
} catch (error) {
window.alert((error as Error).message);
}
})();
return;
}
const url = asset.signed_url ?? asset.file_url;
if (!url) {
window.alert("暂无可用的文件链接");