Files
leaudit-platform-frontend/app/components/reviews/previewComponents/DocxPreviewTest.tsx
T

225 lines
8.2 KiB
TypeScript

/**
* DOCX 预览组件(设计稿 7c-简化C-极简 · 中栏实现)
*
* 使用 CollaboraViewer 渲染 DOCX 文件。
* 工具栏与 PdfPreviewTest 一致,额外增加清除高亮、返回顶部按钮。
*/
import { useState, useEffect, useMemo, useRef } from 'react';
import { CollaboraViewer } from '~/components/collabora/CollaboraViewer';
import type { CollaboraViewerHandle } from '~/components/collabora/types';
import { customGotoPage } from '~/components/collabora/lib';
import { toastService } from '~/components/ui/Toast';
import type { ReviewPoint } from '../ReviewPointsList';
interface DocxPreviewTestProps {
filePath: string;
targetPage?: number;
charPositions?: Array<{ box: number[][]; char: string; score: number }>;
activeReviewPointResultId?: string | null;
reviewPoints?: ReviewPoint[];
highlightValue?: string;
aiSuggestionReplace?: {
searchText: string;
replaceText: string;
pageNumber: number;
silentReplace?: boolean;
};
userInfo?: { sub: string; nick_name: string };
}
export function DocxPreviewTest({
filePath,
targetPage,
activeReviewPointResultId,
reviewPoints,
highlightValue,
aiSuggestionReplace,
userInfo,
}: DocxPreviewTestProps) {
const collaboraRef = useRef<CollaboraViewerHandle>(null);
const [pageInputValue, setPageInputValue] = useState('');
const [isClearingHighlights, setIsClearingHighlights] = useState(false);
const [isScrollingToTop, setIsScrollingToTop] = useState(false);
// 当前激活的评查点
const activePoint = useMemo<ReviewPoint | undefined>(
() => reviewPoints?.find(p => p.id === activeReviewPointResultId),
[reviewPoints, activeReviewPointResultId],
);
// 当前高亮标签
const highlightLabel = useMemo(() => {
if (!activePoint) return null;
const code = activePoint.pointCode || activePoint.id;
const name = activePoint.pointName || activePoint.title || '';
return `${code}${name ? ' · ' + name : ''}`;
}, [activePoint]);
// ── 页码跳转 ──
const handlePageInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPageInputValue(e.target.value.replace(/\D/g, ''));
};
const handlePageJump = async () => {
if (!pageInputValue) return;
const targetPageNum = parseInt(pageInputValue, 10);
const iframeWindow = collaboraRef.current?.getIframeWindow?.();
if (!iframeWindow) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
if (targetPageNum > 0) {
try {
await customGotoPage(iframeWindow, targetPageNum);
setPageInputValue('');
} catch (error) {
toastService.error(`跳转失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
};
// ── 清除高亮 ──
const handleClearAllHighlights = async () => {
if (!collaboraRef.current?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
setIsClearingHighlights(true);
try {
await collaboraRef.current.clearAllHighlights();
toastService.success('已清除所有高亮');
} catch (error) {
console.error('[DocxPreviewTest] 清除高亮失败:', error);
toastService.error('清除高亮失败');
} finally {
setTimeout(() => setIsClearingHighlights(false), 500);
}
};
// ── 返回顶部 ──
const handleScrollToTop = async () => {
setIsScrollingToTop(true);
try {
await collaboraRef.current?.unoCommands.scrollToTop();
} catch (error) {
console.error('[DocxPreviewTest] 返回顶部失败:', error);
toastService.error('返回顶部失败');
} finally {
setTimeout(() => setIsScrollingToTop(false), 500);
}
};
// ── 跳转到高亮页 ──
const jumpToHighlight = () => {
if (!activePoint) return;
// 触发 targetPage 重新跳转(由父组件控制)
toastService.info('已定位到当前评查点所在页');
};
return (
<section className="flex flex-col flex-1 min-h-0 w-full h-full bg-slate-100 border border-slate-200 rounded">
{/* ═══ 顶部工具栏 ═══ */}
<div className="shrink-0 h-11 px-4 flex items-center justify-between bg-white border-b border-slate-200 text-[12.5px] text-slate-600">
<div className="flex items-center gap-2">
{/* 页码跳转 */}
<button
onClick={() => {
const iframeWindow = collaboraRef.current?.getIframeWindow?.();
if (!iframeWindow) return;
// 上一页:跳转到当前页-1
customGotoPage(iframeWindow, -1).catch(() => {});
}}
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 text-slate-400"
title="上一页"
>
<i className="ri-arrow-left-s-line" />
</button>
<span className="font-mono tabular-nums">
<input
type="text"
value={pageInputValue}
onChange={handlePageInputChange}
onFocus={e => e.currentTarget.select()}
onBlur={handlePageJump}
onKeyDown={e => { if (e.key === 'Enter') handlePageJump(); }}
className="w-8 h-6 text-center bg-slate-50 border border-slate-200 rounded text-[12px] font-mono outline-none focus:border-[#00684a]"
placeholder="-"
/>
</span>
<button
onClick={() => {
const iframeWindow = collaboraRef.current?.getIframeWindow?.();
if (!iframeWindow) return;
customGotoPage(iframeWindow, 99999).catch(() => {});
}}
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 text-slate-400"
title="下一页"
>
<i className="ri-arrow-right-s-line" />
</button>
<span className="mx-0 text-slate-300">|</span>
{/* 返回顶部 */}
<button
onClick={handleScrollToTop}
disabled={isScrollingToTop}
className={`w-5 h-7 grid place-items-center rounded hover:bg-slate-100 ${isScrollingToTop ? 'opacity-50 cursor-not-allowed' : ''}`}
title="返回顶部"
>
{isScrollingToTop ? <i className="ri-loader-4-line text-[12px] animate-spin" /> : <i className="ri-arrow-up-double-line" />}
</button>
{/* 清除高亮 */}
<button
onClick={handleClearAllHighlights}
disabled={isClearingHighlights}
className={`w-5 h-7 grid place-items-center rounded hover:bg-slate-100 ${isClearingHighlights ? 'opacity-50 cursor-not-allowed' : ''}`}
title="清除高亮"
>
{isClearingHighlights ? <i className="ri-loader-4-line text-[12px] animate-spin" /> : <i className="ri-eraser-line" />}
</button>
</div>
{/* 右侧:当前高亮标签 */}
<div className="flex items-center gap-2 text-[11.5px] min-w-0">
{highlightLabel && (
<>
<span className="text-slate-400 shrink-0">:</span>
<span
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-amber-50 text-amber-800 border border-amber-200 max-w-[220px]"
title={highlightLabel}
>
<i className="ri-focus-3-line shrink-0" />
<span className="truncate">{highlightLabel}</span>
</span>
<button
onClick={jumpToHighlight}
className="w-7 h-7 grid place-items-center rounded hover:bg-slate-100 shrink-0"
title="跳转到高亮页"
>
<i className="ri-crosshair-line" />
</button>
</>
)}
</div>
</div>
{/* ═══ Collabora 文档区域 ═══ */}
<div className="flex-1 min-h-0">
<CollaboraViewer
ref={collaboraRef}
fileId={filePath}
mode="edit"
userId={userInfo?.sub || 'unknown'}
userName={userInfo?.nick_name || ''}
targetPage={targetPage}
highlightText={highlightValue}
aiSuggestionReplace={aiSuggestionReplace}
/>
</div>
</section>
);
}