feat: 初步完成评查详情页面的work文档和pdf文档的加载的页面三栏设计的重构。
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@@ -352,32 +352,31 @@ export function PdfPreviewTest({
|
||||
file={fileUrl}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={onDocumentLoadError}
|
||||
className="flex flex-col min-h-0 w-full"
|
||||
className="flex flex-col min-h-0 w-full h-full"
|
||||
loading={<div className="flex-1 grid place-items-center text-slate-400 text-sm">PDF 加载中…</div>}
|
||||
error={<div className="flex-1 grid place-items-center text-red-500">PDF 文档加载失败</div>}
|
||||
noData={<div className="flex-1 grid place-items-center text-slate-400">无数据</div>}
|
||||
>
|
||||
<section
|
||||
className="flex flex-col min-h-0 bg-slate-100 border border-slate-200 rounded"
|
||||
style={{ height: 'calc(100vh - 120px)' }}
|
||||
className="flex flex-col flex-1 min-h-0 w-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={() => setShowThumbs(s => !s)}
|
||||
className={`w-7 h-7 grid place-items-center rounded hover:bg-slate-100 ${
|
||||
className={`w-5 h-7 grid place-items-center rounded hover:bg-slate-100 ${
|
||||
showThumbs ? 'text-primary' : 'text-slate-400'
|
||||
}`}
|
||||
title="显示/隐藏页面缩略图"
|
||||
>
|
||||
<i className="ri-layout-masonry-line"></i>
|
||||
</button>
|
||||
<span className="mx-0.5 text-slate-300">|</span>
|
||||
<span className="mx-0 text-slate-300">|</span>
|
||||
<button
|
||||
onClick={goPrev}
|
||||
disabled={currentPage <= 1}
|
||||
className="w-7 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title="上一页"
|
||||
>
|
||||
<i className="ri-arrow-left-s-line"></i>
|
||||
@@ -392,23 +391,23 @@ export function PdfPreviewTest({
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') handlePageJump();
|
||||
}}
|
||||
className="w-9 h-6 text-center bg-slate-50 border border-slate-200 rounded text-[12px] font-mono outline-none focus:border-primary"
|
||||
className="w-5 h-6 text-center bg-slate-50 border border-slate-200 rounded text-[12px] font-mono outline-none focus:border-primary"
|
||||
/>
|
||||
<span className="text-slate-400"> / {numPages ?? '-'}</span>
|
||||
</span>
|
||||
<button
|
||||
onClick={goNext}
|
||||
disabled={!numPages || currentPage >= numPages}
|
||||
className="w-7 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title="下一页"
|
||||
>
|
||||
<i className="ri-arrow-right-s-line"></i>
|
||||
</button>
|
||||
<span className="mx-2 text-slate-300">|</span>
|
||||
<span className="mx-0 text-slate-300">|</span>
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
disabled={zoomLevel <= 50}
|
||||
className="w-7 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40"
|
||||
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40"
|
||||
title="缩小"
|
||||
>
|
||||
<i className="ri-zoom-out-line"></i>
|
||||
@@ -417,7 +416,7 @@ export function PdfPreviewTest({
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
disabled={zoomLevel >= 200}
|
||||
className="w-7 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40"
|
||||
className="w-5 h-7 grid place-items-center rounded hover:bg-slate-100 disabled:opacity-40"
|
||||
title="放大"
|
||||
>
|
||||
<i className="ri-zoom-in-line"></i>
|
||||
@@ -511,9 +510,9 @@ export function PdfPreviewTest({
|
||||
}
|
||||
|
||||
const frameCls = isCur
|
||||
? 'ring-2 ring-primary shadow-md'
|
||||
? 'ring-2 ring-[#00684a] shadow-md'
|
||||
: effThumbMode === 'all' && isRulePage
|
||||
? 'ring-1 ring-primary/40 shadow-sm'
|
||||
? 'ring-1 ring-[#00684a]/40 shadow-sm'
|
||||
: 'border border-slate-200 group-hover:border-slate-400 shadow-sm';
|
||||
|
||||
let fieldsLabel: React.ReactNode = null;
|
||||
@@ -553,7 +552,7 @@ export function PdfPreviewTest({
|
||||
</div>
|
||||
<div
|
||||
className={`text-center text-[10.5px] mt-1 ${
|
||||
isCur ? 'text-primary font-semibold' : 'text-slate-500 group-hover:text-slate-700'
|
||||
isCur ? 'text-[#00684a] font-semibold' : 'text-slate-500 group-hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{p}
|
||||
@@ -570,15 +569,16 @@ export function PdfPreviewTest({
|
||||
{/* ── 视口(单页) ── */}
|
||||
<div
|
||||
ref={viewportRef}
|
||||
className="flex-1 min-h-0 min-w-0 overflow-auto p-4 text-center"
|
||||
className="flex-1 min-h-0 min-w-0 overflow-auto p-4"
|
||||
style={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-start' }}
|
||||
>
|
||||
<div
|
||||
className="pdf-main-canvas"
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
margin: '0 auto',
|
||||
textAlign: 'left',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{numPages !== null && (
|
||||
|
||||
Reference in New Issue
Block a user