temp:备份代码

This commit is contained in:
PingChuan
2025-11-22 19:02:53 +08:00
parent 7e7648383e
commit 31614374a7
5 changed files with 419 additions and 59 deletions
+119 -17
View File
@@ -10,30 +10,63 @@
* @encoding UTF-8
*/
import { useRef } from 'react';
import type { CollaboraViewerProps } from './types';
import { useRef, forwardRef, useImperativeHandle, useState, useEffect } from 'react';
import type { CollaboraViewerProps, CollaboraViewerHandle } from './types';
import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks';
import { sendUnoCommand } from './Uno';
/**
* Collabora 文档查看器组件
* @param props - 组件属性
* @param ref - 父组件传入的 ref,用于暴露命令接口
*/
export function CollaboraViewer({
fileId,
mode = 'view',
userId = 'guest',
userName = '访客',
}: CollaboraViewerProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewerProps>(
function CollaboraViewer(
{
fileId,
mode = 'view',
userId = 'guest',
userName = '访客',
},
ref
) {
const iframeRef = useRef<HTMLIFrameElement>(null);
// 1. 加载 Collabora 配置
const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName);
// 调试面板状态
const [unoCmd, setUnoCmd] = useState('.uno:GoToStartOfDoc');
const [unoArgs, setUnoArgs] = useState('{}');
const [unoResult, setUnoResult] = useState<string | null>(null);
// 2. 监听文档加载状态
const { isDocumentLoaded } = useDocumentReady(iframeRef);
// 1. 加载 Collabora 配置
const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName);
// 3. UNO 命令封装
const unoCommands = useCollaboraUnoCommands(iframeRef);
// 2. 监听文档加载状态
const { isDocumentLoaded } = useDocumentReady(iframeRef);
// 3. UNO 命令封装
const unoCommands = useCollaboraUnoCommands(iframeRef);
// 4. 暴露接口给父组件
useImperativeHandle(ref, () => ({
unoCommands,
isReady: isDocumentLoaded,
mode,
}), [unoCommands, isDocumentLoaded, mode]);
// 5. 将 sendUnoCommand 挂载到 window 对象,供调试面板和控制台使用
useEffect(() => {
if (iframeRef.current?.contentWindow) {
(window as any).sendUno = (cmd: string, args: any = {}) => {
if (iframeRef.current?.contentWindow) {
sendUnoCommand(iframeRef.current.contentWindow, cmd, args);
}
};
}
return () => {
delete (window as any).sendUno;
};
}, [isDocumentLoaded]);
// 加载中状态
if (loading) {
@@ -60,8 +93,75 @@ export function CollaboraViewer({
);
}
// 发送 UNO 命令的处理函数
const sendUno = () => {
if (!iframeRef.current?.contentWindow) {
setUnoResult('iframe 不可用');
return;
}
if (!(window as any).sendUno) {
setUnoResult('window.sendUno 未初始化');
return;
}
let args: any = {};
const raw = (unoArgs || '').trim();
if (raw !== '') {
try {
args = JSON.parse(raw);
} catch (err) {
try {
// fallback: replace single quotes with double quotes and parse
args = JSON.parse(raw.replace(/'(.*?)'/g, '"$1"'));
} catch (err2) {
console.error('解析 UNO Args 失败:', err2);
setUnoResult('Args 解析失败,请使用有效 JSON');
return;
}
}
}
try {
// 先让 iframe 获得焦点
iframeRef.current.focus();
console.log('[调试面板] 已聚焦 iframe');
(window as any).sendUno?.(unoCmd, args);
setUnoResult(`已发送: ${unoCmd}`);
} catch (e) {
console.error('发送 UNO 失败:', e);
setUnoResult('发送失败,请查看控制台');
}
};
return (
<div className="collabora-viewer relative w-full h-full min-h-[600px]">
{/* UNO 命令测试面板 */}
<div className="absolute top-2 left-2 z-50 bg-white bg-opacity-90 px-2 py-1 rounded shadow flex items-center gap-2">
<input
className="px-2 py-1 border rounded text-sm w-48"
value={unoCmd}
onChange={(e) => setUnoCmd(e.target.value)}
placeholder="UNO 命令"
aria-label="UNO 命令"
/>
<input
className="px-2 py-1 border rounded text-sm w-64"
value={unoArgs}
onChange={(e) => setUnoArgs(e.target.value)}
placeholder="UNO Args (JSON)"
aria-label="UNO Args (JSON)"
/>
<button
className="px-3 py-1 bg-indigo-600 text-white rounded hover:bg-indigo-700 text-sm"
onClick={sendUno}
>
UNO
</button>
{unoResult && <span className="text-xs text-gray-500 ml-2">{unoResult}</span>}
</div>
{/* 文档加载提示 */}
{!isDocumentLoaded && (
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-90 z-10">
@@ -85,10 +185,12 @@ export function CollaboraViewer({
allow="clipboard-read; clipboard-write"
title={`Collabora Online - ${config.fileName}`}
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals"
tabIndex={0}
/>
</div>
);
}
});
// 导出 UNO 命令 hook 供父组件使用(如果需要)
// 导出类型和 hook
export type { CollaboraViewerHandle };
export { useCollaboraUnoCommands };
+78 -3
View File
@@ -19,7 +19,6 @@ export function sendUnoCommand(
): void {
const message = {
MessageId: 'Send_UNO_Command',
SendTime: Date.now(),
Values: {
Command: command,
Args: args,
@@ -130,10 +129,25 @@ export function unoEscape(iframeWindow: Window): void {
}
/**
* 滚动到文档开头
* 滚动到文档开头 (带焦点请求)
* @param iframeWindow - iframe 的 contentWindow
*/
export function unoScrollToTop(iframeWindow: Window): void {
export async function unoScrollToTop(iframeWindow: Window): Promise<void> {
// 1. 先请求 iframe 获取焦点
const focusMessage = {
MessageId: 'custompostMessage',
Values: {
Command: 'REQUEST_FOCUS',
Args: {},
},
};
console.log('[custompostMessage] 请求焦点 (滚动到顶部)');
iframeWindow.postMessage(JSON.stringify(focusMessage), '*');
// 2. 等待焦点激活
await new Promise((resolve) => setTimeout(resolve, 100));
// 3. 发送滚动命令
sendUnoCommand(iframeWindow, '.uno:GoToStartOfDoc', {});
}
@@ -166,3 +180,64 @@ export function unoGetState(iframeWindow: Window): void {
console.log('[UNO] 发送 Get_State (.uno:ModifiedStatus) - 等待命令队列执行完成');
iframeWindow.postMessage(JSON.stringify(message), '*');
}
/**
* 放大文档(固定步长)
* @param iframeWindow - iframe 的 contentWindow
*/
export function unoZoomPlus(iframeWindow: Window): void {
sendUnoCommand(iframeWindow, '.uno:ZoomPlus', {});
}
/**
* 缩小文档(固定步长)
* @param iframeWindow - iframe 的 contentWindow
*/
export function unoZoomMinus(iframeWindow: Window): void {
sendUnoCommand(iframeWindow, '.uno:ZoomMinus', {});
}
/**
* 设置文档缩放比例
* @param iframeWindow - iframe 的 contentWindow
* @param percentage - 缩放百分比(例如:100 表示 100%)
*/
export function unoSetZoom(iframeWindow: Window, percentage: number): void {
sendUnoCommand(iframeWindow, '.uno:Zoom', {
Zoom: {
type: 'short',
value: percentage,
},
});
}
/**
* 跳转到指定页面
* @param iframeWindow - iframe 的 contentWindow
* @param pageNumber - 页码(从1开始)
*/
export function unoGotoPage(iframeWindow: Window, pageNumber: number): void {
sendUnoCommand(iframeWindow, '.uno:GotoPage', {
Page: {
type: 'long',
value: pageNumber,
},
});
}
/**
* 跳转到第一页
* @param iframeWindow - iframe 的 contentWindow
*/
export function unoFirstPage(iframeWindow: Window): void {
sendUnoCommand(iframeWindow, '.uno:FirstPage', {});
}
/**
* 跳转到最后一页
* @param iframeWindow - iframe 的 contentWindow
*/
export function unoLastPage(iframeWindow: Window): void {
sendUnoCommand(iframeWindow, '.uno:LastPage', {});
}
+117 -11
View File
@@ -21,6 +21,12 @@ import {
unoEscape,
unoScrollToTop,
unoSave,
unoZoomPlus,
unoZoomMinus,
unoSetZoom,
unoGotoPage,
unoFirstPage,
unoLastPage,
} from './Uno';
import { COLLABORA_URL } from '~/config/api-config';
@@ -131,9 +137,8 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 搜索文本: "${text}"`);
unoSearchText(iframeRef.current.contentWindow, text);
await unoSearchText(iframeRef.current.contentWindow, text);
await new Promise((resolve) => setTimeout(resolve, 100));
},
[iframeRef]
@@ -175,9 +180,8 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 替换文本: "${searchText}" -> "${replaceText}"`);
unoReplaceText(iframeRef.current.contentWindow, searchText, replaceText);
await unoReplaceText(iframeRef.current.contentWindow, searchText, replaceText);
await new Promise((resolve) => setTimeout(resolve, 200));
},
[iframeRef]
@@ -192,9 +196,8 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 高亮文本: "${text}"`);
unoHighlightText(iframeRef.current.contentWindow, text, color);
await unoHighlightText(iframeRef.current.contentWindow, text, color);
await new Promise((resolve) => setTimeout(resolve, 200));
},
[iframeRef]
@@ -209,9 +212,8 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 移除高亮: "${text}"`);
unoRemoveHighlight(iframeRef.current.contentWindow, text);
await unoRemoveHighlight(iframeRef.current.contentWindow, text);
await new Promise((resolve) => setTimeout(resolve, 200));
},
[iframeRef]
@@ -227,7 +229,7 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
}
console.log('[UNO] 取消选中');
unoEscape(iframeRef.current.contentWindow);
await unoEscape(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 50));
}, [iframeRef]);
@@ -241,7 +243,7 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
}
console.log('[UNO] 滚动到顶部');
unoScrollToTop(iframeRef.current.contentWindow);
await unoScrollToTop(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
@@ -255,10 +257,102 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
}
console.log('[UNO] 保存文档');
unoSave(iframeRef.current.contentWindow);
await unoSave(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 1000));
}, [iframeRef]);
/**
* 放大文档
*/
const zoomIn = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 放大文档');
await unoZoomPlus(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
/**
* 缩小文档
*/
const zoomOut = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 缩小文档');
await unoZoomMinus(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
/**
* 设置缩放比例
* @param percentage - 缩放百分比 (例如:100 表示 100%)
*/
const setZoom = useCallback(
async (percentage: number) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 设置缩放比例: ${percentage}%`);
await unoSetZoom(iframeRef.current.contentWindow, percentage);
await new Promise((resolve) => setTimeout(resolve, 100));
},
[iframeRef]
);
/**
* 跳转到指定页面
* @param pageNumber - 页码(从1开始)
*/
const gotoPage = useCallback(
async (pageNumber: number) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 跳转到第 ${pageNumber}`);
await unoGotoPage(iframeRef.current.contentWindow, pageNumber);
await new Promise((resolve) => setTimeout(resolve, 100));
},
[iframeRef]
);
/**
* 跳转到第一页
*/
const gotoFirstPage = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 跳转到第一页');
await unoFirstPage(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
/**
* 跳转到最后一页
*/
const gotoLastPage = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 跳转到最后一页');
await unoLastPage(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
return useMemo(
() => ({
searchText,
@@ -269,6 +363,12 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
escapeSelection,
scrollToTop,
saveDocument,
zoomIn,
zoomOut,
setZoom,
gotoPage,
gotoFirstPage,
gotoLastPage,
}),
[
searchText,
@@ -279,6 +379,12 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
escapeSelection,
scrollToTop,
saveDocument,
zoomIn,
zoomOut,
setZoom,
gotoPage,
gotoFirstPage,
gotoLastPage,
]
);
}
+27
View File
@@ -35,3 +35,30 @@ export interface CollaboraViewerProps {
/** 用户名称 */
userName?: string;
}
/**
* CollaboraViewer 暴露给父组件的方法接口
*/
export interface CollaboraViewerHandle {
/** UNO 命令方法集合 */
unoCommands: {
searchText: (text: string) => Promise<void>;
locateText: (text: string) => Promise<void>;
replaceText: (searchText: string, replaceText: string) => Promise<void>;
highlightText: (text: string, color?: number) => Promise<void>;
removeHighlight: (text: string) => Promise<void>;
escapeSelection: () => Promise<void>;
scrollToTop: () => Promise<void>;
saveDocument: () => Promise<void>;
zoomIn: () => Promise<void>;
zoomOut: () => Promise<void>;
setZoom: (percentage: number) => Promise<void>;
gotoPage: (pageNumber: number) => Promise<void>;
gotoFirstPage: () => Promise<void>;
gotoLastPage: () => Promise<void>;
};
/** 文档是否已加载完成 */
isReady: boolean;
/** 当前模式 */
mode: 'view' | 'edit';
}
+78 -28
View File
@@ -5,7 +5,7 @@
import { useState, useEffect, useRef, ChangeEvent } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import { DOCUMENT_URL } from '~/api/axios-client';
import { CollaboraViewer } from '~/components/collabora/CollaboraViewer';
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
// 设置worker路径为public目录下的worker文件
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
@@ -85,10 +85,17 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
const [zoomLevel, setZoomLevel] = useState(100);
// const [highlightsVisible, setHighlightsVisible] = useState(true);
const contentRef = useRef<HTMLDivElement>(null);
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
const [numPages, setNumPages] = useState<number | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [pageInputValue, setPageInputValue] = useState<string>('');
// 获取文件类型
const real_path = fileContent.path || fileContent.template_contract_path || '';
const fileExtension = real_path.split('.').pop()?.toLowerCase();
const isDocx = fileExtension === 'docx';
const isPdf = fileExtension === 'pdf';
// 拖拽状态管理
const [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式
const [isDragging, setIsDragging] = useState(false);
@@ -97,15 +104,35 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// 放大文档
const handleZoomIn = () => {
if (zoomLevel < 200) {
setZoomLevel(prevZoom => prevZoom + 10);
if (isDocx) {
// DOCX 文件:调用 Collabora UNO 命令
if (!collaboraViewerRef.current?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
collaboraViewerRef.current?.unoCommands.zoomIn();
} else if (isPdf) {
// PDF 文件:修改 zoomLevel 状态
if (zoomLevel < 200) {
setZoomLevel(prevZoom => prevZoom + 10);
}
}
};
// 缩小文档
const handleZoomOut = () => {
if (zoomLevel > 50) {
setZoomLevel(prevZoom => prevZoom - 10);
if (isDocx) {
// DOCX 文件:调用 Collabora UNO 命令
if (!collaboraViewerRef.current?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
collaboraViewerRef.current?.unoCommands.zoomOut();
} else if (isPdf) {
// PDF 文件:修改 zoomLevel 状态
if (zoomLevel > 50) {
setZoomLevel(prevZoom => prevZoom - 10);
}
}
};
@@ -243,21 +270,37 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// 处理页码跳转
const handlePageJump = () => {
if (!pageInputValue || !numPages) return;
if (!pageInputValue) return;
const targetPageNum = parseInt(pageInputValue, 10);
// 验证页码是否在有效范围内
if (targetPageNum > 0 && targetPageNum <= numPages) {
// 找到目标页面元素并滚动到该位置
const pageElement = document.getElementById(`page-${targetPageNum}`);
if (pageElement) {
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
if (isDocx) {
// DOCX 文件:调用 Collabora UNO 命令
if (!collaboraViewerRef.current?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
if (targetPageNum > 0) {
collaboraViewerRef.current?.unoCommands.gotoPage(targetPageNum);
} else {
toastService.warning('请输入有效页码');
setPageInputValue('');
}
} else if (isPdf) {
// PDF 文件:验证页码并滚动到目标页面
if (!numPages) return;
if (targetPageNum > 0 && targetPageNum <= numPages) {
// 找到目标页面元素并滚动到该位置
const pageElement = document.getElementById(`page-${targetPageNum}`);
if (pageElement) {
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
} else {
// 页码超出范围,显示错误信息或重置输入
toastService.warning(`请输入有效页码 (1-${numPages})`);
setPageInputValue('');
}
} else {
// 页码超出范围,显示错误信息或重置输入
toastService.warning(`请输入有效页码 (1-${numPages})`);
setPageInputValue('');
}
};
@@ -285,8 +328,18 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// 滚动到顶部
const handleScrollToTop = () => {
if (contentRef.current) {
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
if (isDocx) {
// DOCX 文件:调用 Collabora UNO 命令
if (!collaboraViewerRef.current?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
collaboraViewerRef.current?.unoCommands.scrollToTop();
} else {
// PDF 文件:滚动容器到顶部
if (contentRef.current) {
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
}
}
};
@@ -385,8 +438,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// 渲染文档内容
const renderDocumentContent = () => {
const real_path = fileContent.path || fileContent.template_contract_path || '';
// 如果路径无效,显示错误信息
if (!real_path) {
if(!fileContent.template_contract_path){
@@ -404,9 +455,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
}
// console.log('real_path',real_path);
// 获取文件扩展名
const fileExtension = real_path.split('.').pop()?.toLowerCase();
// PDF内容渲染
const renderPdfContent = () => (
<div
@@ -470,8 +519,9 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
// DOCX文件使用Collabora Online预览
return (
<CollaboraViewer
ref={collaboraViewerRef}
fileId={real_path}
mode="view"
mode="edit"
userId={userInfo?.sub || 'guest'}
userName={userInfo?.nick_name || '访客'}
/>