fix: restore mobile review comparison navigation
This commit is contained in:
parent
3ceab7ac0b
commit
94641795cf
@ -68,6 +68,15 @@ function createAnnotationId() {
|
||||
return `point-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
}
|
||||
|
||||
function scrollPaneTo(element: HTMLDivElement | null, left: number, behavior: ScrollBehavior) {
|
||||
if (!element) return
|
||||
if (typeof element.scrollTo === 'function') {
|
||||
element.scrollTo({ left, behavior })
|
||||
return
|
||||
}
|
||||
element.scrollLeft = left
|
||||
}
|
||||
|
||||
function LongImagePane({
|
||||
label,
|
||||
groups,
|
||||
@ -198,7 +207,7 @@ function LongImagePane({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain bg-[#eef1f5] p-2 sm:p-3">
|
||||
<div className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain bg-[#eef1f5] p-2 sm:p-3">
|
||||
{activeGroup ? (
|
||||
<div
|
||||
className={`relative mx-auto max-w-[1200px] overflow-hidden bg-white shadow-[0_8px_28px_rgba(15,23,42,0.08)] ${annotationMode ? 'cursor-crosshair touch-pan-y outline outline-2 outline-[#d49a31]' : ''}`}
|
||||
@ -295,6 +304,7 @@ export function WorkReviewReviewerWorkspace({
|
||||
const [selectedAnnotation, setSelectedAnnotation] = useState<{ imageIndex: number; id: string } | null>(null)
|
||||
const [annotationMode, setAnnotationMode] = useState(false)
|
||||
const [showAnnotationComments, setShowAnnotationComments] = useState(true)
|
||||
const [activeMobilePane, setActiveMobilePane] = useState<0 | 1>(0)
|
||||
const [busyAction, setBusyAction] = useState<'approved' | 'revision_required' | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [liveMessage, setLiveMessage] = useState('')
|
||||
@ -307,6 +317,8 @@ export function WorkReviewReviewerWorkspace({
|
||||
[latestVersion, project.versions, selectedVersionId],
|
||||
)
|
||||
const workImageCount = getImageCount(selectedVersion)
|
||||
const hasReferenceImages = (selectedVersion?.referenceImageGroups?.length || 0) > 0
|
||||
|| (selectedVersion?.referenceImageUrls?.length || 0) > 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!project.versions.some((version) => version.id === selectedVersionId)) {
|
||||
@ -320,9 +332,30 @@ export function WorkReviewReviewerWorkspace({
|
||||
setSelectedAnnotation(null)
|
||||
setAnnotationMode(false)
|
||||
setShowAnnotationComments(true)
|
||||
setActiveMobilePane(0)
|
||||
setError(null)
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
scrollPaneTo(mobileScrollerRef.current, 0, 'auto')
|
||||
})
|
||||
return () => window.cancelAnimationFrame(frame)
|
||||
}, [selectedVersion?.id, workImageCount])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasReferenceImages) return
|
||||
setActiveMobilePane(0)
|
||||
scrollPaneTo(mobileScrollerRef.current, 0, 'auto')
|
||||
}, [hasReferenceImages])
|
||||
|
||||
useEffect(() => {
|
||||
const alignActivePane = () => {
|
||||
const scroller = mobileScrollerRef.current
|
||||
if (!scroller) return
|
||||
scrollPaneTo(scroller, scroller.clientWidth * activeMobilePane, 'auto')
|
||||
}
|
||||
window.addEventListener('resize', alignActivePane)
|
||||
return () => window.removeEventListener('resize', alignActivePane)
|
||||
}, [activeMobilePane])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAnnotation) return
|
||||
const frame = window.requestAnimationFrame(() => annotationInputRef.current?.focus())
|
||||
@ -333,8 +366,6 @@ export function WorkReviewReviewerWorkspace({
|
||||
|
||||
const isLatestSelected = selectedVersion.id === latestVersion.id
|
||||
const pendingRole = isLatestSelected ? project.permissions.pendingReviewRoles[0] : undefined
|
||||
const hasReferenceImages = (selectedVersion.referenceImageGroups?.length || 0) > 0
|
||||
|| (selectedVersion.referenceImageUrls?.length || 0) > 0
|
||||
const currentDrafts = draftAnnotations[activeWorkImageIndex] || []
|
||||
const selectedDraft = selectedAnnotation
|
||||
? draftAnnotations[selectedAnnotation.imageIndex]?.find((item) => item.id === selectedAnnotation.id)
|
||||
@ -367,10 +398,15 @@ export function WorkReviewReviewerWorkspace({
|
||||
setWorkPanePercent(Math.min(MAX_PANE_PERCENT, Math.max(MIN_PANE_PERCENT, Math.round(percent))))
|
||||
}
|
||||
|
||||
const moveMobilePane = (index: number) => {
|
||||
const moveMobilePane = (index: 0 | 1) => {
|
||||
const scroller = mobileScrollerRef.current
|
||||
if (!scroller) return
|
||||
scroller.scrollTo({ left: scroller.clientWidth * index, behavior: 'smooth' })
|
||||
setActiveMobilePane(index)
|
||||
if (index === 1) {
|
||||
setAnnotationMode(false)
|
||||
setSelectedAnnotation(null)
|
||||
}
|
||||
scrollPaneTo(scroller, scroller.clientWidth * index, 'smooth')
|
||||
}
|
||||
|
||||
const addAnnotation = (x: number, y: number) => {
|
||||
@ -516,6 +552,37 @@ export function WorkReviewReviewerWorkspace({
|
||||
aria-labelledby={`work-review-version-tab-${selectedVersion.id}`}
|
||||
className="flex h-[calc(100dvh-144px)] min-h-[600px] flex-col overflow-hidden border border-[#dfe5ee] bg-white"
|
||||
>
|
||||
{hasReferenceImages && (
|
||||
<div className="shrink-0 border-b border-[#dfe5ee] bg-white p-2 md:hidden">
|
||||
<div
|
||||
className="grid grid-cols-2 gap-1 rounded bg-[#eef2f6] p-1"
|
||||
role="group"
|
||||
aria-label="切换作品图与参考图"
|
||||
>
|
||||
{([
|
||||
{ index: 0 as const, label: '作品图' },
|
||||
{ index: 1 as const, label: '参考图' },
|
||||
]).map((item) => {
|
||||
const active = activeMobilePane === item.index
|
||||
return (
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
onClick={() => moveMobilePane(item.index)}
|
||||
aria-pressed={active}
|
||||
className={`min-h-11 rounded px-4 text-[13px] font-bold outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[#0050cb] focus-visible:ring-offset-1 ${
|
||||
active
|
||||
? 'bg-[#dcecff] text-[#0050cb]'
|
||||
: 'bg-transparent text-[#526173] hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<div
|
||||
ref={comparisonRef}
|
||||
@ -619,10 +686,28 @@ export function WorkReviewReviewerWorkspace({
|
||||
className="flex h-full snap-x snap-mandatory overflow-x-auto overscroll-x-contain md:hidden"
|
||||
aria-label={hasReferenceImages ? '作品与参考图片对照区,左右滑动切换' : '作品图片'}
|
||||
tabIndex={hasReferenceImages ? 0 : undefined}
|
||||
onScroll={(event) => {
|
||||
if (!hasReferenceImages || event.currentTarget.clientWidth <= 0) return
|
||||
const nextPane = Math.min(
|
||||
1,
|
||||
Math.max(0, Math.round(event.currentTarget.scrollLeft / event.currentTarget.clientWidth)),
|
||||
) as 0 | 1
|
||||
setActiveMobilePane((current) => current === nextPane ? current : nextPane)
|
||||
if (nextPane === 1 && annotationMode) {
|
||||
setAnnotationMode(false)
|
||||
setSelectedAnnotation(null)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!hasReferenceImages) return
|
||||
if (event.key === 'ArrowLeft') moveMobilePane(0)
|
||||
if (event.key === 'ArrowRight') moveMobilePane(1)
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault()
|
||||
moveMobilePane(0)
|
||||
}
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
moveMobilePane(1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-full shrink-0 snap-start">
|
||||
|
||||
@ -45,6 +45,23 @@ const project: WorkReviewProject = {
|
||||
},
|
||||
}
|
||||
|
||||
const projectWithReference: WorkReviewProject = {
|
||||
...project,
|
||||
versions: [{
|
||||
...project.versions[0],
|
||||
referenceImageGroups: [{
|
||||
imageId: 'reference-1',
|
||||
sourceName: '参考长图',
|
||||
previewUrl: '/reference-preview.webp',
|
||||
previewWidth: 1000,
|
||||
previewHeight: 2000,
|
||||
width: 1000,
|
||||
height: 2000,
|
||||
slices: [{ url: '/reference-slice.webp', width: 1000, height: 2000 }],
|
||||
}],
|
||||
}],
|
||||
}
|
||||
|
||||
describe('WorkReviewReviewerWorkspace', () => {
|
||||
test('adds a normalized point annotation and submits the current review for revision', async () => {
|
||||
window.PointerEvent = MouseEvent as typeof PointerEvent
|
||||
@ -118,4 +135,33 @@ describe('WorkReviewReviewerWorkspace', () => {
|
||||
}],
|
||||
})
|
||||
})
|
||||
|
||||
test('keeps mobile work and reference controls synchronized with the comparison scroller', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<WorkReviewReviewerWorkspace project={projectWithReference} onSubmitReview={jest.fn()} />)
|
||||
|
||||
const scroller = screen.getByLabelText('作品与参考图片对照区,左右滑动切换')
|
||||
const scrollTo = jest.fn()
|
||||
Object.defineProperty(scroller, 'clientWidth', { configurable: true, value: 360 })
|
||||
Object.defineProperty(scroller, 'scrollTo', { configurable: true, value: scrollTo })
|
||||
|
||||
const workButton = screen.getByRole('button', { name: '作品图' })
|
||||
const referenceButton = screen.getByRole('button', { name: '参考图' })
|
||||
expect(workButton).toHaveAttribute('aria-pressed', 'true')
|
||||
expect(referenceButton).toHaveAttribute('aria-pressed', 'false')
|
||||
|
||||
await user.click(referenceButton)
|
||||
expect(scrollTo).toHaveBeenLastCalledWith({ left: 360, behavior: 'smooth' })
|
||||
expect(referenceButton).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
Object.defineProperty(scroller, 'scrollLeft', { configurable: true, value: 0 })
|
||||
fireEvent.scroll(scroller)
|
||||
expect(workButton).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
test('does not show comparison controls when the version has no reference image', () => {
|
||||
render(<WorkReviewReviewerWorkspace project={project} onSubmitReview={jest.fn()} />)
|
||||
expect(screen.queryByRole('button', { name: '作品图' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '参考图' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user