0.3.5 共享功能修复
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user