refactor(intake): extract review issue image manager

This commit is contained in:
WuYongJie 2026-06-09 15:20:56 +08:00
parent df580ae8bd
commit b02bb244ec
2 changed files with 180 additions and 137 deletions

View File

@ -18,7 +18,7 @@ import {
useRef,
useState,
} from "react";
import { ArrowLeft, Ban, ChevronDown, Plus, X } from "lucide-react";
import { ArrowLeft, Ban, ChevronDown, Plus } from "lucide-react";
import CameraScannerModal from "@/components/mobile/CameraScannerModal";
import { ModelPreviewPanel } from "@/components/platform/ModelPreviewPanel";
import Button from "@/components/ui/Button";
@ -57,6 +57,7 @@ import { ProductDataWorkspace } from "./ProductDataWorkspace";
import { QualityInspectionWorkspace } from "./QualityInspectionWorkspace";
import { QualityStandardWorkspace } from "./QualityStandardWorkspace";
import { ReviewIssueImageCell } from "./ReviewIssueImageCell";
import { ReviewIssueImageManagerDialog } from "./ReviewIssueImageManagerDialog";
import { SampleMeasurementWorkspace } from "./SampleMeasurementWorkspace";
import {
BomBoardMaterialField,
@ -19145,142 +19146,25 @@ export default function NonIntegratedDetailClient({
</div>
{reviewImageManager && managedReviewIssue ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/35 px-4 py-6">
<div
data-testid={`review-sheet-${reviewImageManager.target}-image-manager-paste-zone-${managedReviewIssue.id}`}
tabIndex={canEditReviewSheet ? 0 : undefined}
autoFocus={canEditReviewSheet}
aria-label={`${managedReviewImageLabel}管理粘贴区`}
title={
canEditReviewSheet
? `Ctrl+V 继续粘贴${managedReviewImageLabel}`
: undefined
}
onPaste={(event) =>
void handleReviewIssueImagePaste(
managedReviewIssue.id,
reviewImageManager.target,
event,
)
}
className="flex max-h-[86vh] w-full max-w-[720px] flex-col rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-lg outline-none focus-visible:ring-2 focus-visible:ring-apple-blue/35"
>
<div className="flex items-center justify-between gap-3 border-b border-apple-separator px-4 py-3">
<div>
<p className="text-sm font-apple-medium text-apple-label-primary">
管理{managedReviewImageLabel}
</p>
<p className="mt-1 text-xs text-apple-label-secondary">
共 {managedReviewImages.length} 张,标记为导出主图的图片会优先显示
</p>
</div>
<button
type="button"
onClick={() => setReviewImageManager(null)}
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-apple-separator bg-apple-bg-secondary/40 text-apple-label-secondary transition hover:bg-apple-bg-secondary"
aria-label="关闭图片管理"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-4">
{managedReviewImages.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2">
{managedReviewImages.map((image) => (
<div
key={image.id}
className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary/20 p-2"
>
<button
type="button"
onClick={() =>
setWorkspaceImagePreview({
kind: "standalone",
src: image.url,
alt: image.fileName || managedReviewImageLabel,
})
}
className="flex aspect-square w-full items-center justify-center overflow-hidden rounded-apple-lg border border-apple-separator bg-white"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={image.url}
alt={image.fileName || managedReviewImageLabel}
className="h-full w-full object-contain"
/>
</button>
<div className="mt-2 flex items-center justify-between gap-2">
<p className="min-w-0 truncate text-xs text-apple-label-primary">
{image.fileName || managedReviewImageLabel}
</p>
{image.isPrimary ? (
<span className="shrink-0 rounded-full bg-zinc-950 px-2 py-0.5 text-[11px] text-white">
导出主图
</span>
) : null}
</div>
{canEditReviewSheet ? (
<div className="mt-2 flex flex-wrap items-center justify-center gap-2">
<button
type="button"
onClick={() =>
handleSetReviewIssuePrimaryImage(
managedReviewIssue.id,
reviewImageManager.target,
image.id,
)
}
className="inline-flex h-8 items-center justify-center rounded-apple-lg border border-apple-separator bg-apple-bg-primary px-2.5 text-xs text-apple-label-primary transition hover:bg-apple-bg-secondary/50"
disabled={image.isPrimary}
>
设为导出主图
</button>
<button
type="button"
onClick={() =>
handleDeleteReviewIssueImage(
managedReviewIssue.id,
reviewImageManager.target,
image.id,
)
}
className="inline-flex h-8 items-center justify-center rounded-apple-lg border border-apple-separator bg-apple-bg-primary px-2.5 text-xs text-apple-label-primary transition hover:bg-apple-bg-secondary/50"
>
删除
</button>
</div>
) : null}
</div>
))}
</div>
) : (
<div className="flex min-h-[180px] items-center justify-center rounded-apple-lg border border-dashed border-apple-separator bg-apple-bg-secondary/20 text-xs text-apple-label-secondary">
暂无图片
</div>
)}
</div>
{canEditReviewSheet ? (
<div className="flex items-center justify-end border-t border-apple-separator px-4 py-3">
<label className="inline-flex h-9 cursor-pointer items-center justify-center rounded-apple-lg border border-zinc-950 bg-zinc-950 px-3 text-xs font-apple-medium text-white">
继续上传
<input
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(event) =>
void handleReviewIssueImageChange(
managedReviewIssue.id,
reviewImageManager.target,
event,
)
}
/>
</label>
</div>
) : null}
</div>
</div>
<ReviewIssueImageManagerDialog
issue={managedReviewIssue}
target={reviewImageManager.target}
label={managedReviewImageLabel}
images={managedReviewImages}
canEdit={canEditReviewSheet}
onClose={() => setReviewImageManager(null)}
onPaste={handleReviewIssueImagePaste}
onPreviewImage={(src, alt) =>
setWorkspaceImagePreview({
kind: "standalone",
src,
alt,
})
}
onSetPrimaryImage={handleSetReviewIssuePrimaryImage}
onDeleteImage={handleDeleteReviewIssueImage}
onImageChange={handleReviewIssueImageChange}
/>
) : null}
</>
);

View File

@ -0,0 +1,159 @@
"use client";
import type { ChangeEvent, ClipboardEvent } from "react";
import { X } from "lucide-react";
import type {
NonIntegratedReviewImage,
NonIntegratedReviewIssue,
} from "./nonIntegratedStorage";
type ReviewIssueImageTarget = "issue" | "reference";
type ReviewIssueImageManagerDialogProps = {
issue: NonIntegratedReviewIssue;
target: ReviewIssueImageTarget;
label: string;
images: NonIntegratedReviewImage[];
canEdit: boolean;
onClose: () => void;
onPaste: (
issueId: string,
target: ReviewIssueImageTarget,
event: ClipboardEvent<HTMLDivElement>,
) => void | Promise<void>;
onPreviewImage: (src: string, alt: string) => void;
onSetPrimaryImage: (
issueId: string,
target: ReviewIssueImageTarget,
imageId: string,
) => void;
onDeleteImage: (
issueId: string,
target: ReviewIssueImageTarget,
imageId: string,
) => void;
onImageChange: (
issueId: string,
target: ReviewIssueImageTarget,
event: ChangeEvent<HTMLInputElement>,
) => void | Promise<void>;
};
export function ReviewIssueImageManagerDialog({
issue,
target,
label,
images,
canEdit,
onClose,
onPaste,
onPreviewImage,
onSetPrimaryImage,
onDeleteImage,
onImageChange,
}: ReviewIssueImageManagerDialogProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/35 px-4 py-6">
<div
data-testid={`review-sheet-${target}-image-manager-paste-zone-${issue.id}`}
tabIndex={canEdit ? 0 : undefined}
autoFocus={canEdit}
aria-label={`${label}管理粘贴区`}
title={canEdit ? `Ctrl+V 继续粘贴${label}` : undefined}
onPaste={(event) => void onPaste(issue.id, target, event)}
className="flex max-h-[86vh] w-full max-w-[720px] flex-col rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-lg outline-none focus-visible:ring-2 focus-visible:ring-apple-blue/35"
>
<div className="flex items-center justify-between gap-3 border-b border-apple-separator px-4 py-3">
<div>
<p className="text-sm font-apple-medium text-apple-label-primary">
管理{label}
</p>
<p className="mt-1 text-xs text-apple-label-secondary">
共 {images.length} 张,标记为导出主图的图片会优先显示
</p>
</div>
<button
type="button"
onClick={onClose}
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-apple-separator bg-apple-bg-secondary/40 text-apple-label-secondary transition hover:bg-apple-bg-secondary"
aria-label="关闭图片管理"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-4">
{images.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2">
{images.map((image) => (
<div
key={image.id}
className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary/20 p-2"
>
<button
type="button"
onClick={() => onPreviewImage(image.url, image.fileName || label)}
className="flex aspect-square w-full items-center justify-center overflow-hidden rounded-apple-lg border border-apple-separator bg-white"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={image.url}
alt={image.fileName || label}
className="h-full w-full object-contain"
/>
</button>
<div className="mt-2 flex items-center justify-between gap-2">
<p className="min-w-0 truncate text-xs text-apple-label-primary">
{image.fileName || label}
</p>
{image.isPrimary ? (
<span className="shrink-0 rounded-full bg-zinc-950 px-2 py-0.5 text-[11px] text-white">
导出主图
</span>
) : null}
</div>
{canEdit ? (
<div className="mt-2 flex flex-wrap items-center justify-center gap-2">
<button
type="button"
onClick={() => onSetPrimaryImage(issue.id, target, image.id)}
className="inline-flex h-8 items-center justify-center rounded-apple-lg border border-apple-separator bg-apple-bg-primary px-2.5 text-xs text-apple-label-primary transition hover:bg-apple-bg-secondary/50"
disabled={image.isPrimary}
>
设为导出主图
</button>
<button
type="button"
onClick={() => onDeleteImage(issue.id, target, image.id)}
className="inline-flex h-8 items-center justify-center rounded-apple-lg border border-apple-separator bg-apple-bg-primary px-2.5 text-xs text-apple-label-primary transition hover:bg-apple-bg-secondary/50"
>
删除
</button>
</div>
) : null}
</div>
))}
</div>
) : (
<div className="flex min-h-[180px] items-center justify-center rounded-apple-lg border border-dashed border-apple-separator bg-apple-bg-secondary/20 text-xs text-apple-label-secondary">
暂无图片
</div>
)}
</div>
{canEdit ? (
<div className="flex items-center justify-end border-t border-apple-separator px-4 py-3">
<label className="inline-flex h-9 cursor-pointer items-center justify-center rounded-apple-lg border border-zinc-950 bg-zinc-950 px-3 text-xs font-apple-medium text-white">
继续上传
<input
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(event) => void onImageChange(issue.id, target, event)}
/>
</label>
</div>
) : null}
</div>
</div>
);
}