Next.js 14(App Router) React文本标注span错位问题求解
法律判决书标注位置错位问题求解
我正在开发一款基于React Next.js 14(App Router)的应用,支持用户为法律判决书添加评论标注。当前实现逻辑是:用户选中判决书文本区域后,通过标注表单收集内容,然后在选中文本的起止位置插入span标签做高亮。
但现在遇到一个问题:添加第一个标注时一切正常,但后续添加的span标签位置会出现错位,我无法定位问题所在,恳请提供更优的span标签位置计算方案。
JudgmentDisplay组件代码
'use client'; import React, { useRef, useEffect, useState } from 'react'; import { Judgment, Note, Selection } from '../../../types'; interface JudgmentDisplayProps { judgment: Judgment; notes: Note[]; onTextSelect: (selection: Selection) => void; onNoteClick: (note: Note) => void; } export default function JudgmentDisplay({ judgment, notes, onTextSelect, onNoteClick }: JudgmentDisplayProps) { const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const handleMouseUp = (event: React.MouseEvent, fieldName: string) => { const selection = window.getSelection(); if (selection && selection.toString().length > 0) { const range = selection.getRangeAt(0); const contentElement = contentRefs.current[fieldName]; if (contentElement) { const preCaretRange = document.createRange(); preCaretRange.selectNodeContents(contentElement); preCaretRange.setEnd(range.startContainer, range.startOffset); const start = preCaretRange.toString().length; const end = start + selection.toString().length; console.log(`Selected in ${fieldName}: "${selection.toString()}", start: ${start}, end: ${end}`); onTextSelect({ text: selection.toString(), start, end, fieldName, }); } } }; const renderField = (fieldName: string, content: string) => { const fieldNotes = notes.filter(note => note.field_name === fieldName); const segments: React.ReactNode[] = []; let lastIndex = 0; fieldNotes.sort((a, b) => a.start_index - b.start_index).forEach(note => { if (note.start_index > lastIndex) { segments.push(content.slice(lastIndex, note.start_index)); } segments.push( <React.Fragment key={note.id}> <span className="cursor-pointer text-blue-500" onClick={() => onNoteClick(note)} > * </span> <span className="bg-yellow-200"> {content.slice(note.start_index, note.end_index)} </span> </React.Fragment> ); lastIndex = note.end_index; }); if (lastIndex < content.length) { segments.push(content.slice(lastIndex)); } return ( <div key={fieldName} ref={(el) => { if (el) { contentRefs.current[fieldName] = el; } }} className="mb-4" > <h2 className="text-2xl font-bold mb-2">{fieldName}</h2> <div className="whitespace-pre-wrap" onMouseUp={(e) => handleMouseUp(e, fieldName)} > {segments} </div> </div> ); }; return ( <div> {Object.entries(judgment).map(([fieldName, content]) => typeof content === 'string' ? renderField(fieldName, content) : null )} </div> ); }
表单处理代码
'use client'; import React, { useState, useEffect, useCallback } from 'react'; import { useUser } from '@clerk/nextjs'; import JudgmentSelector from './_components/judgments/judgmentSelector'; import JudgmentDisplay from './_components/judgments/judgmentDisplay'; import NoteForm from './_components/notes/noteForm'; import NotePanel from './_components/notes/notePanel'; import { Judgment, Note, Selection } from '../types'; import { fetchJudgments, fetchNotes, addNote, editNote, deleteNote } from './actions'; export default function Home() { const { user } = useUser(); const [judgments, setJudgments] = useState<Judgment[]>([]); const [selectedJudgment, setSelectedJudgment] = useState<Judgment | null>(null); const [notes, setNotes] = useState<Note[]>([]); const [selection, setSelection] = useState<Selection | null>(null); const [isNoteFormOpen, setIsNoteFormOpen] = useState(false); const [selectedNote, setSelectedNote] = useState<Note | null>(null); useEffect(() => { if (user) { fetchJudgments().then(setJudgments); } }, [user]); useEffect(() => { if (selectedJudgment) { fetchNotes(selectedJudgment.id).then(setNotes); } else { setNotes([]); } }, [selectedJudgment]); const handleJudgmentSelect = (judgment: Judgment) => { setSelectedJudgment(judgment); setSelection(null); setIsNoteFormOpen(false); setSelectedNote(null); }; const handleTextSelect = (newSelection: Selection) => { console.log(`Selected text: "${newSelection.text}", start: ${newSelection.start}, end: ${newSelection.end}, field: ${newSelection.fieldName}`); setSelection(newSelection); setIsNoteFormOpen(true); }; const handleNoteSubmit = async (comment: string) => { if (selection && selectedJudgment) { try { console.log(`Creating note in ${selection.fieldName}: "${selection.text}" at ${selection.start}-${selection.end}`); const newNote = await addNote(comment, selection.start, selection.end, selectedJudgment.id, selection.fieldName); console.log('New note created:', newNote); setNotes(prevNotes => [...prevNotes, newNote]); setIsNoteFormOpen(false); setSelection(null); } catch (error) { console.error("Error adding note:", error); } } }; const handleNoteClick = useCallback((note: Note) => { console.log('Note clicked:', note); setSelectedNote(note); }, []); const handleNoteUpdate = async (updatedNote: Note) => { try { console.log('Updating note:', updatedNote); const result = await editNote(updatedNote.id, updatedNote.comment, updatedNote.judgment_id); console.log('Note updated:', result); setNotes(prevNotes => prevNotes.map(n => n.id === result.id ? result : n)); } catch (error) { console.error("Error updating note:", error); } }; const handleNoteDelete = async (noteId: number) => { if (selectedJudgment) { try { console.log('Deleting note:', noteId); await deleteNote(noteId, selectedJudgment.id); console.log('Note deleted:', noteId); setNotes(prevNotes => prevNotes.filter(n => n.id !== noteId)); setSelectedNote(null); } catch (error) { console.error("Error deleting note:", error); } } }; if (!user) { return <div>Please sign in to access this page.</div>; } return ( <div className="container mx-auto p-4"> <JudgmentSelector judgments={judgments} selectedJudgment={selectedJudgment} onSelect={handleJudgmentSelect} /> {selectedJudgment && ( <JudgmentDisplay judgment={selectedJudgment} notes={notes} onTextSelect={handleTextSelect} onNoteClick={handleNoteClick} /> )} {isNoteFormOpen && selection && ( <NoteForm selection={selection} onSubmit={handleNoteSubmit} onClose={() => { setIsNoteFormOpen(false); setSelection(null); }} /> )} <NotePanel note={selectedNote} onClose={() => setSelectedNote(null)} onNoteUpdate={handleNoteUpdate} onNoteDelete={handleNoteDelete} /> </div> ); }
内容的提问来源于stack exchange,提问作者Mel
相关产品推荐
相关产品推荐

