gongxiangp2/components/work-review/WorkReviewAnnotationMarker.tsx

166 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import type { WorkReviewImageAnnotation, WorkReviewRole } from '@/types/work-review'
export type WorkReviewDisplayAnnotation = {
annotation: WorkReviewImageAnnotation
markerNumber: number
reviewerName?: string
reviewerRole?: WorkReviewRole
draft?: boolean
positionEditable?: boolean
}
function markerTone(item: WorkReviewDisplayAnnotation) {
if (item.draft) return 'bg-[#8a5200] text-white ring-[#fff3d6]'
if (item.reviewerRole === 'final_reviewer') return 'bg-[#087a3c] text-white ring-[#d9f6e5]'
return 'bg-[#0050cb] text-white ring-[#dcecff]'
}
function markerColor(item: WorkReviewDisplayAnnotation) {
if (item.annotation.color) return item.annotation.color
if (item.draft) return '#8a5200'
if (item.reviewerRole === 'final_reviewer') return '#087a3c'
return '#0050cb'
}
function commentPosition(x: number) {
return x > 0.62 ? 'right-9' : 'left-9'
}
export function WorkReviewAnnotationMarker({
item,
onClick,
showComment = true,
}: {
item: WorkReviewDisplayAnnotation
onClick?: () => void
showComment?: boolean
}) {
const { annotation } = item
const shape = annotation.shape || 'point'
const color = markerColor(item)
const alignRight = annotation.x > 0.62
const roleLabel = item.draft
? '当前批注'
: item.reviewerRole === 'final_reviewer'
? '元泽'
: '评审人'
const summary = annotation.comment.trim() || '待填写修改意见'
const positionState = item.positionEditable
? shape === 'point' ? ',位置可调整' : ',范围可调整'
: ''
const label = `${roleLabel}标注 ${item.markerNumber}${positionState}${item.reviewerName ? `,${item.reviewerName}` : ''}:${summary}`
const visualTitle = showComment ? label : `${roleLabel}标注 ${item.markerNumber}`
if (shape !== 'point' && annotation.x2 != null && annotation.y2 != null) {
const minX = Math.min(annotation.x, annotation.x2)
const minY = Math.min(annotation.y, annotation.y2)
const width = Math.max(0.008, Math.abs(annotation.x2 - annotation.x))
const height = Math.max(0.008, Math.abs(annotation.y2 - annotation.y))
const arrowPointsRight = annotation.x2 >= annotation.x
const arrowPointsDown = annotation.y2 >= annotation.y
const arrowX1 = arrowPointsRight ? 4 : 96
const arrowY1 = arrowPointsDown ? 4 : 96
const arrowX2 = arrowPointsRight ? 96 : 4
const arrowY2 = arrowPointsDown ? 96 : 4
const markerId = `work-review-arrow-${annotation.id.replace(/[^a-zA-Z0-9_-]/g, '')}`
const lightColor = color === '#ffffff'
return (
<button
type="button"
onClick={(event) => {
event.stopPropagation()
onClick?.()
}}
onPointerDown={(event) => event.stopPropagation()}
onPointerUp={(event) => event.stopPropagation()}
aria-label={label}
title={visualTitle}
className={`group absolute z-20 min-h-5 min-w-5 outline-none ${item.positionEditable ? 'cursor-crosshair' : 'cursor-pointer'} focus-visible:ring-2 focus-visible:ring-[#8ab9ff] focus-visible:ring-offset-2`}
style={{
left: `${minX * 100}%`,
top: `${minY * 100}%`,
width: `${width * 100}%`,
height: `${height * 100}%`,
}}
>
{shape === 'arrow' ? (
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="absolute inset-0 h-full w-full overflow-visible drop-shadow-[0_2px_3px_rgba(15,23,42,0.34)]"
aria-hidden="true"
>
<defs>
<marker id={markerId} markerWidth="12" markerHeight="12" refX="9" refY="5" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L10,5 L0,10 Z" fill={color} stroke={lightColor ? '#344054' : color} strokeWidth={lightColor ? 1 : 0} />
</marker>
</defs>
<line
x1={arrowX1}
y1={arrowY1}
x2={arrowX2}
y2={arrowY2}
stroke={color}
strokeWidth="4"
vectorEffect="non-scaling-stroke"
markerEnd={`url(#${markerId})`}
/>
</svg>
) : (
<span
className={`absolute inset-0 border-[3px] shadow-[0_2px_8px_rgba(15,23,42,0.24)] ${shape === 'ellipse' ? 'rounded-[50%]' : 'rounded-sm'} ${lightColor ? 'outline outline-1 outline-[#344054]' : ''}`}
style={{ borderColor: color }}
aria-hidden="true"
/>
)}
<span
className={`absolute -left-3 -top-3 flex h-7 w-7 items-center justify-center rounded-full border-2 text-[11px] font-black shadow-[0_3px_10px_rgba(15,23,42,0.35)] ${lightColor ? 'border-[#344054] text-[#101828]' : 'border-white text-white'} ${item.positionEditable ? 'ring-2 ring-[#8ab9ff] ring-offset-2' : ''}`}
style={{ backgroundColor: color }}
aria-hidden="true"
>
{item.markerNumber}
</span>
<span
className={`pointer-events-none absolute -top-0.5 w-max max-w-[min(10rem,calc(100vw-3rem))] whitespace-normal break-words rounded-lg bg-[#101828]/90 px-3 py-2 text-left text-[11px] font-semibold leading-[1.45] text-white shadow-[0_4px_14px_rgba(15,23,42,0.24)] [overflow-wrap:anywhere] sm:max-w-[min(14rem,calc(100vw-3rem))] ${showComment ? 'block' : 'hidden'} ${commentPosition(minX)}`}
aria-hidden={!showComment}
>
{summary}
</span>
</button>
)
}
return (
<button
type="button"
onClick={(event) => {
event.stopPropagation()
onClick?.()
}}
onPointerDown={(event) => event.stopPropagation()}
onPointerUp={(event) => event.stopPropagation()}
aria-label={label}
title={visualTitle}
className={`group absolute z-20 h-11 w-11 -translate-x-1/2 -translate-y-1/2 outline-none ${item.positionEditable ? 'cursor-move' : 'cursor-pointer'}`}
style={{ left: `${annotation.x * 100}%`, top: `${annotation.y * 100}%` }}
>
<span className={`absolute left-1/2 top-1/2 flex h-7 w-7 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full text-[11px] font-black shadow-[0_3px_10px_rgba(15,23,42,0.35)] ring-4 ${markerTone(item)} ${item.positionEditable ? 'outline outline-2 outline-offset-2 outline-[#8a5200]' : ''} group-focus-visible:ring-[#8ab9ff]`}>
{item.markerNumber}
</span>
<span
className={`pointer-events-none absolute top-1/2 w-max max-w-[min(10rem,calc(100vw-3rem))] -translate-y-1/2 whitespace-normal break-words rounded-lg bg-[#101828]/90 px-3 py-2 text-left text-[11px] font-semibold leading-[1.45] text-white shadow-[0_4px_14px_rgba(15,23,42,0.24)] [overflow-wrap:anywhere] sm:max-w-[min(14rem,calc(100vw-3rem))] ${
showComment ? 'block' : 'hidden'
} ${
alignRight ? 'right-9' : 'left-9'
}`}
aria-hidden={!showComment}
>
{summary}
</span>
</button>
)
}