diff --git a/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.jsx b/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.jsx index 3361c7a..27f770c 100644 --- a/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.jsx +++ b/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.jsx @@ -79,6 +79,7 @@ export default function AiPlagiarismCheck() { const processedRef = useRef(new Set()); const warnedRef = useRef(new Set()); const nameMapRef = useRef({}); + const scrollSyncRef = useRef(false); /* ---- 派生 ---- */ const visibleIndices = useMemo( @@ -308,6 +309,82 @@ export default function AiPlagiarismCheck() { setEditContent(block.htmlContent || block.plainText); }, [taskFiles, fileBlocksCache, taskId]); + // 点击一侧匹配段落时,跳转到另一侧对应段落并高亮 + const jumpToCounterpart = useCallback((docIdx, idx) => { + const related = matches.filter( + (m) => + (m.leftIdx === docIdx && m.leftPara === idx) || + (m.rightIdx === docIdx && m.rightPara === idx), + ); + if (!related.length) return; + const match = related[0]; + setCurrentFocusId(match.id); + const targetDocIdx = match.leftIdx === docIdx ? match.rightIdx : match.leftIdx; + const targetPara = match.leftIdx === docIdx ? match.rightPara : match.leftPara; + const targetId = "doc-container-" + targetDocIdx + "-" + targetPara; + + // 被隐藏的文件没有 DOM,先恢复面板,再在目标 pane 内滚动,避免 scrollIntoView 滚动外层横向容器。 + if (hiddenFiles.has(targetDocIdx)) { + setHiddenFiles((prev) => { + const next = new Set(prev); + next.delete(targetDocIdx); + return next; + }); + } + + requestAnimationFrame(() => { + const targetEl = document.getElementById(targetId); + const targetPane = targetEl?.closest("." + styles.pane); + if (!targetEl || !targetPane) return; + + const paneRect = targetPane.getBoundingClientRect(); + const targetRect = targetEl.getBoundingClientRect(); + const targetTop = targetPane.scrollTop + targetRect.top - paneRect.top; + const centeredTop = targetTop - (targetPane.clientHeight - targetEl.offsetHeight) / 2; + targetPane.scrollTo({ top: Math.max(0, centeredTop), behavior: "smooth" }); + }); + }, [matches, hiddenFiles, styles.pane]); + + // 按中缝匹配关系同步双屏滚动,不按两个文档总高度做比例同步。 + const syncCounterpartScroll = useCallback((docIdx, sourcePane) => { + if (scrollSyncRef.current || !sourcePane || !matches.length) return; + + const sourcePaneRect = sourcePane.getBoundingClientRect(); + const candidates = matches + .filter((match) => match.leftIdx === docIdx || match.rightIdx === docIdx) + .map((match) => { + const sourcePara = match.leftIdx === docIdx ? match.leftPara : match.rightPara; + const targetDocIdx = match.leftIdx === docIdx ? match.rightIdx : match.leftIdx; + const targetPara = match.leftIdx === docIdx ? match.rightPara : match.leftPara; + const sourceEl = document.getElementById("doc-container-" + docIdx + "-" + sourcePara); + const targetEl = document.getElementById("doc-container-" + targetDocIdx + "-" + targetPara); + if (!sourceEl || !targetEl) return null; + return { + sourceEl, + targetEl, + distance: Math.abs(sourceEl.getBoundingClientRect().top - sourcePaneRect.top), + }; + }) + .filter(Boolean) + .sort((a, b) => a.distance - b.distance); + + const current = candidates[0]; + const targetPane = current?.targetEl.closest("." + styles.pane); + if (!current || !targetPane) return; + + const sourceTop = current.sourceEl.getBoundingClientRect().top - sourcePaneRect.top; + const targetPaneRect = targetPane.getBoundingClientRect(); + const targetTop = targetPane.scrollTop + current.targetEl.getBoundingClientRect().top - targetPaneRect.top; + const nextTop = Math.max(0, targetTop - sourceTop); + if (Math.abs(targetPane.scrollTop - nextTop) < 2) return; + + scrollSyncRef.current = true; + targetPane.scrollTop = nextTop; + requestAnimationFrame(() => { + scrollSyncRef.current = false; + }); + }, [matches, styles.pane]); + const applyAiRewrite = useCallback(async () => { if (!editingBlock || !editingBlock.taskId || !editingBlock.fileId || !editingBlock.blockId) { message.warning("无法降重:缺少必要信息"); return; @@ -590,7 +667,12 @@ export default function AiPlagiarismCheck() { const activeClass = isFocused ? " " + styles.active : ""; // CSS覆盖高亮,保留样式;纯文本用 精确标记 if (/<[^>]+>/.test(html)) { - return (

openEditor(docIdx, idx)} dangerouslySetInnerHTML={{ __html: html }} />

); + return ( +
+

jumpToCounterpart(docIdx, idx)} dangerouslySetInnerHTML={{ __html: html }} /> +

+ ); } // 纯文本精确高亮 const spans = []; @@ -614,7 +696,12 @@ export default function AiPlagiarismCheck() { cursor = s.end; }); if (cursor < plain.length) parts.push(plain.slice(cursor)); - return (

openEditor(docIdx, idx)}>{parts}

); + return ( +
+

jumpToCounterpart(docIdx, idx)}>{parts}

+
+ ); } } @@ -669,7 +756,10 @@ export default function AiPlagiarismCheck() { const blocks = fileBlocksCache[file?.id] || []; return (
-
+
{ + syncCounterpartScroll(docIdx, event.currentTarget); + redrawAll(); + }}>
{(nameMapRef.current[file?.originalName] || fixGarbledName(file?.originalName) || "文档" + (docIdx + 1))} diff --git a/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.module.less b/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.module.less index 2b1a4b4..2627363 100644 --- a/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.module.less +++ b/web/client/src/sections/aiTenderDocument/container/aiPlagiarismCheck.module.less @@ -480,6 +480,26 @@ } } +/* 匹配段落外层:承载点击跳转 + 悬浮编辑按钮 */ +.matchParaWrap { + position: relative; +} + +.matchEditBtn { + position: absolute; + top: 4px; + right: 6px; + opacity: 0.4; + background: #fff; + border-radius: 4px; + z-index: 2; + transition: opacity 0.15s ease; +} + +.matchParaWrap:hover .matchEditBtn { + opacity: 1; +} + /* 中缝(面板间连线区域) */ .gutter { width: 48px; diff --git a/web/client/src/sections/superAgent/components/AgentSquare.jsx b/web/client/src/sections/superAgent/components/AgentSquare.jsx index 95e8b22..6d00028 100644 --- a/web/client/src/sections/superAgent/components/AgentSquare.jsx +++ b/web/client/src/sections/superAgent/components/AgentSquare.jsx @@ -158,9 +158,10 @@ const AGENT_CATEGORY_FILTERS = [ { key: "我的收藏", label: "我的收藏" }, ]; -// superAgent 发现智能体仅保留当前已接入的两个业务入口。 +// superAgent 发现智能体仅保留当前已接入的业务入口 const SUPER_AGENT_VISIBLE_CARD_KEYS = new Set([ "stable-finance-invoice", + "stable-plagiarism-check", "beta-message", ]); @@ -280,6 +281,26 @@ const buildSections = (token, userIdParam) => [ icon: duibiIcon, component: "financeInvoice", }, + { + key: "stable-plagiarism-check", + title: "AI查重", + description: + "上传文稿自动检测重复内容,按连续相同字数阈值定位重复段落,支持差异比对与在线修改。", + updatedAt: "2026-08-17", + badgeA: "正式", + badgeB: "分析类", + category: "专业应用", + cornerBadge: "new", + icon: duibiIcon, + component: "aiTenderDocument", + initialActiveMenu: "check", + url: buildAppPath("/aiTenderDocument", { + activeMenu: "check", + hideMenu: "1", + token, + userid: userIdParam, + }), + }, ], }, {