0.3.5 共享功能修复

This commit is contained in:
liaibo
2026-01-24 12:32:51 +08:00
parent 25923f308c
commit 3c3f407f4b
44 changed files with 3754 additions and 420 deletions
@@ -18,6 +18,7 @@ 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 { useCurrentDocumentStore } from "@/store/current-document";
import {
DropdownMenu,
DropdownMenuContent,
@@ -281,6 +282,7 @@ const MediaBlockContent = ({ block, editor }: any) => {
window.alert("缺少 ONLYOFFICE 地址,请在 .env.local 配置 NEXT_PUBLIC_ONLYOFFICE_BASE_URL");
return;
}
const currentDocReadOnly = useCurrentDocumentStore.getState().readOnly;
try {
const assetId = await resolveAssetId();
const res = assetId
@@ -299,16 +301,33 @@ const MediaBlockContent = ({ block, editor }: any) => {
target.searchParams.set("fileUrl", signedUrl);
target.searchParams.set("fileName", displayFileName);
target.searchParams.set("fileType", extension || "docx");
const docId = resolveDocumentId();
if (docId) {
target.searchParams.set("documentId", docId);
}
if (assetId) {
target.searchParams.set("assetId", assetId);
}
target.searchParams.set("mode", currentDocReadOnly ? "view" : "edit");
window.open(target.toString(), "_blank", "noopener,noreferrer");
} catch (error) {
window.alert((error as Error).message);
}
};
const currentDocumentId = useCurrentDocumentStore((state) => state.documentId);
const currentDisableDownload = useCurrentDocumentStore((state) => state.disableDownload);
const resolvedDocIdForRestriction = resolveDocumentId();
const downloadDisabled =
Boolean(currentDisableDownload) &&
Boolean(resolvedDocIdForRestriction) &&
String(currentDocumentId ?? "") === String(resolvedDocIdForRestriction);
const downloadAsset = async () => {
if (downloadDisabled) {
window.alert("该页面已禁止下载");
return;
}
const url = await resolveLatestFileUrl();
if (!url) return;
const anchor = document.createElement("a");
@@ -449,8 +468,9 @@ const MediaBlockContent = ({ block, editor }: any) => {
type="button"
data-testid="wolai-media-file-download"
className="inline-flex h-6 w-6 items-center justify-center rounded-[4px] text-[#9B9A97] hover:bg-[#EDEDED] hover:text-[#37352F]"
aria-label="下载"
title="下载"
aria-label={downloadDisabled ? "已禁止下载" : "下载"}
title={downloadDisabled ? "已禁止下载" : "下载"}
disabled={downloadDisabled}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -497,11 +517,12 @@ const MediaBlockContent = ({ block, editor }: any) => {
</DropdownMenuItem>
{isOfficeDoc && <DropdownMenuItem onClick={() => void openWithOnlyOffice()}>使 ONLYOFFICE </DropdownMenuItem>}
<DropdownMenuItem
disabled={downloadDisabled}
onClick={() => {
void downloadAsset();
}}
>
{downloadDisabled ? "已禁止下载" : "下载到本地"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDeleteAsset}></DropdownMenuItem>
@@ -570,14 +591,16 @@ const MediaBlockContent = ({ block, editor }: any) => {
icon: <LinkIcon className="h-4 w-4" />,
onClick: handleLink,
},
{
key: "download",
label: `下载${typeLabel}`,
icon: <Download className="h-4 w-4" />,
onClick: () => {
void downloadAsset();
},
},
!downloadDisabled
? {
key: "download",
label: `下载${typeLabel}`,
icon: <Download className="h-4 w-4" />,
onClick: () => {
void downloadAsset();
},
}
: null,
{
key: "delete",
label: `删除${typeLabel}`,
@@ -657,11 +680,12 @@ const MediaBlockContent = ({ block, editor }: any) => {
</DropdownMenuItem>
<DropdownMenuItem
disabled={downloadDisabled}
onClick={() => {
void downloadAsset();
}}
>
{downloadDisabled ? "已禁止下载" : "下载到本地"}
</DropdownMenuItem>
{canTriggerOcr && (
<>
@@ -1,12 +1,13 @@
"use client";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent as ReactKeyboardEvent } from "react";
import type { DocumentStats, PageOptionsState } from "@/types/page-options";
import { PageOptionsSidebar } from "@/components/editor/page-options-sidebar";
import { PageBacklinksPanel } from "@/components/editor/page-backlinks-panel";
import { useEditorBridgeStore } from "@/store/editor-bridge";
import { usePageLayoutStore } from "@/store/page-layout";
import { useCurrentDocumentStore } from "@/store/current-document";
import type { Json } from "@/types/supabase";
import { DocumentHistoryDrawer } from "@/components/editor/document-history-drawer";
import type { DocumentSnapshot } from "@/types/document";
@@ -36,6 +37,8 @@ export interface DocumentContentProps {
initialStats: DocumentStats | null;
openTableId?: string | null;
readOnly?: boolean;
disableDownload?: boolean;
disableCopy?: boolean;
}
const defaultOptions: PageOptionsState = {
@@ -59,7 +62,11 @@ export function DocumentContent({
initialStats,
openTableId,
readOnly = false,
disableDownload = false,
disableCopy = false,
}: DocumentContentProps) {
const setCurrentDocument = useCurrentDocumentStore((state) => state.setCurrent);
const clearIfMatch = useCurrentDocumentStore((state) => state.clearIfMatch);
const [options, setOptions] = useState<PageOptionsState>(initialOptions ?? defaultOptions);
const [stats, setStats] = useState<DocumentStats>(initialStats ?? defaultStats);
const [history, setHistory] = useState<DocumentSnapshot[]>([]);
@@ -76,6 +83,78 @@ export function DocumentContent({
const contentLoadingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const pendingOpenTableRef = useRef<string | null>(null);
const latestBlocksRef = useRef<Json | null>(null);
const pageRootRef = useRef<HTMLDivElement>(null);
const lastCopyBlockedAtRef = useRef<number>(0);
useEffect(() => {
setCurrentDocument(documentId, readOnly, disableDownload, disableCopy);
return () => {
clearIfMatch(documentId);
};
}, [clearIfMatch, disableCopy, disableDownload, documentId, readOnly, setCurrentDocument]);
useEffect(() => {
if (!disableCopy) return;
const isEventInsidePage = () => {
const root = pageRootRef.current;
if (!root) return false;
const selection = typeof window !== "undefined" ? window.getSelection() : null;
const anchor = selection?.anchorNode ?? null;
const focus = selection?.focusNode ?? null;
const anchorEl =
anchor && "nodeType" in anchor && anchor.nodeType === Node.TEXT_NODE
? anchor.parentElement
: (anchor as any as Element | null);
const focusEl =
focus && "nodeType" in focus && focus.nodeType === Node.TEXT_NODE
? focus.parentElement
: (focus as any as Element | null);
return Boolean((anchorEl && root.contains(anchorEl)) || (focusEl && root.contains(focusEl)));
};
const notifyBlocked = () => {
const now = Date.now();
if (now - lastCopyBlockedAtRef.current < 1200) return;
lastCopyBlockedAtRef.current = now;
window.alert("该页面已禁止复制");
};
const onCopy = (event: ClipboardEvent) => {
if (!isEventInsidePage()) return;
event.preventDefault();
event.stopPropagation();
notifyBlocked();
};
const onCut = (event: ClipboardEvent) => {
if (!isEventInsidePage()) return;
event.preventDefault();
event.stopPropagation();
notifyBlocked();
};
const onKeyDown = (event: KeyboardEvent) => {
if (!isEventInsidePage()) return;
const key = String(event.key ?? "").toLowerCase();
const ctrlOrMeta = event.ctrlKey || event.metaKey;
if (!ctrlOrMeta) return;
if (key === "c" || key === "x" || key === "insert") {
event.preventDefault();
event.stopPropagation();
notifyBlocked();
}
};
document.addEventListener("copy", onCopy, true);
document.addEventListener("cut", onCut, true);
document.addEventListener("keydown", onKeyDown, true);
return () => {
document.removeEventListener("copy", onCopy, true);
document.removeEventListener("cut", onCut, true);
document.removeEventListener("keydown", onKeyDown, true);
};
}, [disableCopy]);
useEffect(() => {
const tableId = (openTableId ?? "").trim();
@@ -208,7 +287,7 @@ export function DocumentContent({
void persistTitle(pageTitle);
};
const handleTitleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
const handleTitleKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
event.preventDefault();
event.currentTarget.blur();
@@ -251,6 +330,10 @@ export function DocumentContent({
}, [updatedAt]);
const handleExport = useCallback(() => {
if (disableDownload) {
window.alert("该页面已禁止下载");
return;
}
const latest = history[0];
if (!latest) {
window.alert("暂无可导出的内容");
@@ -264,7 +347,7 @@ export function DocumentContent({
anchor.download = `${title ?? "未命名页面"}-${new Date(latest.timestamp).toISOString()}.json`;
anchor.click();
URL.revokeObjectURL(url);
}, [history, title]);
}, [disableDownload, history, title]);
const handleSnapshot = useCallback((payload: { blocks: Json; stats: DocumentStats }) => {
latestBlocksRef.current = payload.blocks;
@@ -315,7 +398,7 @@ export function DocumentContent({
return (
<ImagePickerProvider documentId={documentId} workspaceId={workspaceId}>
<div className="flex h-full overflow-hidden bg-wolai-bg">
<div className="flex h-full overflow-hidden bg-wolai-bg" ref={pageRootRef}>
<div className="flex h-full flex-1 flex-col overflow-hidden">
<div className="border-b border-wolai-border px-12 pb-6 pt-8">
<div className="relative">