166 lines
6.8 KiB
TypeScript
166 lines
6.8 KiB
TypeScript
'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>
|
||
)
|
||
}
|