fix(intake): simplify AI white product photo controls

This commit is contained in:
WuYongJie 2026-06-16 13:53:32 +08:00
parent 351a14d3ad
commit 6eb624f8e4
10 changed files with 117 additions and 106 deletions

View File

@ -95,7 +95,6 @@ describe("NonIntegratedAiWhiteBackgroundPanel", () => {
]}
statusText="待生成"
sessionStatus="draft"
missingSourceSlotLabels={[]}
availableSourceViewCount={1}
canUpload={false}
canUploadToMaterialLibrary
@ -224,7 +223,6 @@ describe("NonIntegratedAiWhiteBackgroundPanel", () => {
]}
statusText="生成中"
sessionStatus="draft"
missingSourceSlotLabels={[]}
availableSourceViewCount={1}
canUpload={false}
canUploadToMaterialLibrary
@ -314,12 +312,32 @@ describe("NonIntegratedAiWhiteBackgroundPanel", () => {
expect(
screen.getByText("所选花色未配置贴图,请先在基础资料库补充贴图。"),
).toBeInTheDocument();
expect(screen.getByText("缺贴图")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "生成已上传角度" }),
screen.getByTestId("ai-white-background-target-finish-swatch"),
).toHaveTextContent("缺贴图");
expect(
screen
.getByTestId("ai-white-background-target-finish-swatch")
.querySelector("img"),
).toBeNull();
expect(
screen.getByRole("button", { name: "缺少花色贴图" }),
).toBeDisabled();
});
it("labels selected target finish generation as a new finish output", () => {
renderPanel({ targetFinishId: "finish-black-walnut" });
expect(
screen.getByRole("button", { name: "生成新花色白底图" }),
).toBeEnabled();
expect(
screen
.getByTestId("ai-white-background-target-finish-swatch")
.querySelector("img"),
).toBeInTheDocument();
});
it("explains read-only product photo permission before blocking operations", () => {
renderPanel({ isReadOnly: true });

View File

@ -10586,15 +10586,12 @@ describe("NonIntegratedDetailClient", () => {
expect(
within(detailList).queryByText("front-ai-white.png"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("product-photo-panel-tab-ai-white-background"),
).not.toBeInTheDocument();
await actor.click(
screen.getByTestId("product-photo-panel-tab-ai-white-background"),
);
const aiArchiveList = await screen.findByTestId("product-photo-detail-list");
expect(within(aiArchiveList).getByText("front-ai-white.png")).toBeInTheDocument();
await actor.click(
screen.getByTestId("product-photo-open-ai-white-background"),
screen.getByTestId("product-photo-workspace-mode-ai-white-background"),
);
expect(
screen.getByTestId("product-photo-ai-white-background-workspace"),
@ -10607,7 +10604,7 @@ describe("NonIntegratedDetailClient", () => {
"BAR-001",
);
expect(
screen.getByText("可生成 1/8"),
screen.getByText("源图 1/8"),
).toBeInTheDocument();
expect(screen.getByText("已生成 1/8")).toBeInTheDocument();
expect(
@ -10681,7 +10678,7 @@ describe("NonIntegratedDetailClient", () => {
await actor.click(screen.getByTestId("document-workspace-trigger-产品实拍图"));
await actor.click(
screen.getByTestId("product-photo-open-ai-white-background"),
screen.getByTestId("product-photo-workspace-mode-ai-white-background"),
);
await actor.click(await screen.findByAltText("front-ai-white.png"));
@ -10689,9 +10686,9 @@ describe("NonIntegratedDetailClient", () => {
expect(
screen.getByRole("dialog", { name: "正视图原图" }),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "关闭预览" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "收起原图" })).toBeInTheDocument();
await actor.click(screen.getByRole("button", { name: "关闭预览" }));
await actor.click(screen.getByRole("button", { name: "收起原图" }));
expect(
screen.queryByRole("dialog", { name: "正视图原图" }),
@ -10774,7 +10771,7 @@ describe("NonIntegratedDetailClient", () => {
await actor.click(screen.getByTestId("document-workspace-trigger-产品实拍图"));
await actor.click(
screen.getByTestId("product-photo-open-ai-white-background"),
screen.getByTestId("product-photo-workspace-mode-ai-white-background"),
);
const aiWhiteBackgroundWorkspace = await screen.findByTestId(
@ -10845,7 +10842,7 @@ describe("NonIntegratedDetailClient", () => {
await actor.click(screen.getByTestId("document-workspace-trigger-产品实拍图"));
await actor.click(
screen.getByTestId("product-photo-open-ai-white-background"),
screen.getByTestId("product-photo-workspace-mode-ai-white-background"),
);
await actor.click(
screen.getByRole("button", { name: "生成已上传角度" }),

View File

@ -4806,7 +4806,6 @@ export default function NonIntegratedDetailClient({
activeProductPhotoPanel,
canDeleteSelectedReviewRound,
canDownloadWorkspaceFiles,
canUseAiWhiteBackground,
canUseProductPricingPushControls,
confirmedClassName: DOCUMENT_WORKSPACE_CONFIRMED_CLASS,
currentProductPhotoGroupUploadLabel,
@ -4905,12 +4904,6 @@ export default function NonIntegratedDetailClient({
handleFinishedSampleShipmentStatusClick,
onImportProductLimitWorkbookChange:
handleImportProductLimitWorkbookChange,
onOpenAiWhiteBackground: () =>
setActiveProductPhotoMode((current) =>
current === "ai-white-background"
? "source"
: "ai-white-background",
),
onOpenProductPhotoCurrentUpload:
openProductPhotoCurrentUpload,
onOpenProductPhotoPanelUpload: openProductPhotoPanelUpload,

View File

@ -12,7 +12,6 @@ import {
} from "react";
import { createPortal } from "react-dom";
import {
AlertCircle,
Check,
ChevronDown,
ImageIcon,
@ -54,7 +53,6 @@ type NonIntegratedAiWhiteBackgroundPanelProps = {
barcodeOptions: AiWhiteBackgroundBarcodeOption[];
statusText: string;
sessionStatus: AiWhiteBackgroundSessionStatus | null;
missingSourceSlotLabels: string[];
availableSourceViewCount: number;
canUpload: boolean;
canUploadToMaterialLibrary: boolean;
@ -788,6 +786,54 @@ function AiWhiteBackgroundCardImage({
);
}
function AiWhiteBackgroundTargetFinishSwatch({
imageUrl,
fallbackColor,
missingTexture,
}: {
imageUrl: string;
fallbackColor: string;
missingTexture: boolean;
}) {
const [failedImageUrl, setFailedImageUrl] = useState<string | null>(null);
const normalizedFallbackColor = fallbackColor.trim();
const canUseFallbackColor = isHexColor(normalizedFallbackColor);
const canShowImage =
Boolean(imageUrl) && !missingTexture && failedImageUrl !== imageUrl;
return (
<span
aria-label={missingTexture ? "花色贴图未配置" : "目标花色预览"}
className={cn(
"mb-0 flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-[6px] bg-[#dee3e6] text-[10px] font-black leading-3 text-[#5a6062] ring-1 ring-[#adb3b5]/22",
missingTexture ? "bg-[#eef1f2] text-[#728083]" : null,
)}
data-testid="ai-white-background-target-finish-swatch"
style={{
backgroundColor:
!missingTexture && !canShowImage && canUseFallbackColor
? normalizedFallbackColor
: undefined,
}}
>
{canShowImage ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imageUrl}
alt=""
className="h-full w-full object-cover"
onError={() => setFailedImageUrl(imageUrl)}
/>
) : missingTexture ? (
<span className="flex flex-col items-center gap-0.5">
<ImageIcon className="h-3.5 w-3.5" aria-hidden="true" />
<span>缺贴图</span>
</span>
) : null}
</span>
);
}
export default function NonIntegratedAiWhiteBackgroundPanel({
styleLabel,
styleKey,
@ -798,7 +844,6 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
targetFinishOptions,
barcodeOptions,
sessionStatus,
missingSourceSlotLabels,
availableSourceViewCount,
canUpload,
canUploadToMaterialLibrary,
@ -846,6 +891,11 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
const targetFinishDisabledReason = targetFinishTextureMissing
? "所选花色未配置贴图,请先在基础资料库补充贴图。"
: null;
const bulkGenerateLabel = targetFinishId
? targetFinishTextureMissing
? "缺少花色贴图"
: "生成新花色白底图"
: "生成已上传角度";
const canOperateAiWhiteBackground = !isReadOnly && canUseAiWhiteBackground;
const operationDisabledReason = isReadOnly
? "当前账号没有产品实拍写权限,无法生成或上传 AI 白底图。"
@ -1119,26 +1169,13 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
options={targetFinishOptions}
className="w-0 min-w-0 flex-1"
/>
<span
aria-hidden="true"
className="mb-0 h-10 w-10 shrink-0 overflow-hidden rounded-[6px] bg-[#dee3e6] ring-1 ring-[#adb3b5]/22"
style={{
backgroundColor:
!selectedTargetFinishPreviewUrl &&
isHexColor(selectedTargetFinish?.color ?? effectiveTargetColor)
? (selectedTargetFinish?.color ?? effectiveTargetColor)
: "#dee3e6",
}}
>
{selectedTargetFinishPreviewUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={selectedTargetFinishPreviewUrl}
alt=""
className="h-full w-full object-cover"
/>
) : null}
</span>
<AiWhiteBackgroundTargetFinishSwatch
imageUrl={selectedTargetFinishPreviewUrl}
fallbackColor={
selectedTargetFinish?.color ?? effectiveTargetColor
}
missingTexture={targetFinishTextureMissing}
/>
</div>
{targetFinishDisabledReason ? (
<p className="text-[11px] font-semibold leading-4 text-[#725737]">
@ -1153,27 +1190,6 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
</div>
) : null}
{missingSourceSlotLabels.length > 0 ? (
<div className="rounded-[6px] bg-[#fbf4e7]/72 px-3 py-2 text-[#725737] ring-1 ring-[#b6905d]/12">
<div className="flex items-start gap-2">
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#9d7a47]" />
<div className="min-w-0">
<p className="text-[11px] font-black">待补角度</p>
<div className="mt-1 flex flex-wrap gap-1">
{missingSourceSlotLabels.map((label) => (
<span
key={label}
className="rounded-[4px] bg-white/68 px-1.5 py-0.5 text-[10px] font-bold leading-4 text-[#725737]/88"
>
{label}
</span>
))}
</div>
</div>
</div>
</div>
) : null}
{!canUploadToMaterialLibrary && uploadDisabledReason ? (
<div className="rounded-[6px] bg-[#fbf4e7]/72 px-3 py-2 text-[11px] font-semibold leading-4 text-[#725737] ring-1 ring-[#b6905d]/12">
{uploadDisabledReason}
@ -1195,10 +1211,15 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
hasAnyGenerationInFlight
}
className="h-10 !min-h-10 rounded-[6px] text-[12px] font-black"
title={
targetFinishId && !targetFinishTextureMissing
? "生成的是同规格新花色白底图,不覆盖当前产品。"
: undefined
}
onClick={() => void onGenerate()}
>
<Sparkles className="h-3.5 w-3.5" />
生成已上传角度
{bulkGenerateLabel}
</Button>
<Button

View File

@ -2,8 +2,6 @@
import { useEffect, useMemo, useState } from "react";
import {
AI_WHITE_BACKGROUND_SOURCE_SLOTS,
AI_WHITE_BACKGROUND_SOURCE_SLOT_LABELS,
AI_WHITE_BACKGROUND_VIEW_KEYS,
AI_WHITE_BACKGROUND_VIEW_KEY_TO_SOURCE_SLOT,
AI_WHITE_BACKGROUND_VIEW_LABELS,
@ -303,14 +301,6 @@ export function NonIntegratedAiWhiteBackgroundWorkspace({
[sourceSlotFiles],
);
const missingSourceSlotLabels = useMemo(
() =>
AI_WHITE_BACKGROUND_SOURCE_SLOTS.filter((slot) => !sourceSlotFiles[slot]).map(
(slot) => AI_WHITE_BACKGROUND_SOURCE_SLOT_LABELS[slot],
),
[sourceSlotFiles],
);
const availableSourceViewKeys = useMemo(
() =>
AI_WHITE_BACKGROUND_VIEW_KEYS.filter((viewKey) => sourceReadyByView[viewKey]),
@ -537,7 +527,6 @@ export function NonIntegratedAiWhiteBackgroundWorkspace({
barcodeOptions={barcodeOptions}
statusText={statusText}
sessionStatus={sessionStatus}
missingSourceSlotLabels={missingSourceSlotLabels}
availableSourceViewCount={availableSourceViewCount}
canUpload={canUpload}
canUploadToMaterialLibrary={canUploadToMaterialLibrary}

View File

@ -10,7 +10,6 @@ import type {
type ProductPhotoHeaderActionsProps = {
actionGroupClassName: string;
isReadOnly: boolean;
canUseAiWhiteBackground: boolean;
canDownloadWorkspaceFiles: boolean;
detailUploadInputRef: RefObject<HTMLInputElement | null>;
currentUploadInputRef: RefObject<HTMLInputElement | null>;
@ -24,7 +23,6 @@ type ProductPhotoHeaderActionsProps = {
downloadFiles: NonIntegratedUploadFile[];
selectedFile: NonIntegratedUploadFile | null;
confirmationAction: ReactNode;
onOpenAiWhiteBackground: () => void;
onOpenPanelUpload: (panel: ProductPhotoGroup) => void;
onOpenCurrentUpload: (target: ProductPhotoSelection) => void;
onPanelUploadChange: (event: ChangeEvent<HTMLInputElement>) => void;
@ -37,7 +35,6 @@ type ProductPhotoHeaderActionsProps = {
export function ProductPhotoHeaderActions({
actionGroupClassName,
isReadOnly,
canUseAiWhiteBackground,
canDownloadWorkspaceFiles,
detailUploadInputRef,
currentUploadInputRef,
@ -51,7 +48,6 @@ export function ProductPhotoHeaderActions({
downloadFiles,
selectedFile,
confirmationAction,
onOpenAiWhiteBackground,
onOpenPanelUpload,
onOpenCurrentUpload,
onPanelUploadChange,
@ -62,15 +58,6 @@ export function ProductPhotoHeaderActions({
}: ProductPhotoHeaderActionsProps) {
return (
<div className={actionGroupClassName}>
<Button
variant={activeMode === "ai-white-background" ? "filled" : "plain"}
size="small"
data-testid="product-photo-open-ai-white-background"
onClick={onOpenAiWhiteBackground}
disabled={!canUseAiWhiteBackground}
>
{activeMode === "ai-white-background" ? "实拍资料" : "AI 白底图"}
</Button>
{isReadOnly || activeMode === "ai-white-background" ? null : (
<>
<Button size="small" onClick={() => onOpenPanelUpload(activePanel)}>

View File

@ -2,7 +2,7 @@ import type { ReactNode } from "react";
import type { NonIntegratedUploadFile } from "../../nonIntegratedStorage";
import {
PRODUCT_PHOTO_NINE_VIEW_SLOTS,
PRODUCT_PHOTO_PANEL_ORDER,
PRODUCT_PHOTO_SOURCE_PANEL_ORDER,
getProductPhotoPanelCompactTitle,
type ProductPhotoFileGroup,
type ProductPhotoGroup,
@ -159,7 +159,7 @@ export function ProductPhotoWorkspace({
<div className="flex min-h-0 flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary">
<div className="border-b border-apple-separator px-2 py-1.5">
<div className="grid grid-cols-5 gap-1">
{PRODUCT_PHOTO_PANEL_ORDER.map((panelKey) => {
{PRODUCT_PHOTO_SOURCE_PANEL_ORDER.map((panelKey) => {
const countLabel =
panelKey === "nine-view"
? `${workspaceData.filledSlotCount} / 9`

View File

@ -324,9 +324,17 @@ export const PRODUCT_PHOTO_FILE_GROUPS: ProductPhotoFileGroup[] = [
"competitor-sample",
"competitor-disassembly",
];
export const PRODUCT_PHOTO_PANEL_ORDER: ProductPhotoGroup[] = [
export const PRODUCT_PHOTO_SOURCE_FILE_GROUPS: Exclude<
ProductPhotoFileGroup,
"ai-white-background"
>[] = [
"detail",
"quality-check",
"competitor-sample",
"competitor-disassembly",
];
export const PRODUCT_PHOTO_SOURCE_PANEL_ORDER: ProductPhotoGroup[] = [
"nine-view",
"ai-white-background",
"detail",
"quality-check",
"competitor-sample",

View File

@ -1,8 +1,8 @@
import { useEffect, type Dispatch, type SetStateAction } from "react";
import type { ProductPhotoWorkspaceData } from "../style-assets/styleWorkspaceEntries";
import {
PRODUCT_PHOTO_FILE_GROUPS,
PRODUCT_PHOTO_NINE_VIEW_SLOTS,
PRODUCT_PHOTO_SOURCE_FILE_GROUPS,
areProductPhotoSelectionsEqual,
type ProductPhotoSelection,
} from "./documentWorkspaceHelpers";
@ -30,13 +30,16 @@ export function useProductPhotoTargetSync({
(slot) => productPhotoWorkspaceData.slotFiles[slot.key],
)?.key ?? null;
const firstFileGroupWithFile =
PRODUCT_PHOTO_FILE_GROUPS.find(
PRODUCT_PHOTO_SOURCE_FILE_GROUPS.find(
(group) => productPhotoWorkspaceData.groupFiles[group][0],
) ?? null;
let nextTarget: ProductPhotoSelection;
if (selectedProductPhotoTarget.group !== "nine-view") {
if (
selectedProductPhotoTarget.group !== "nine-view" &&
selectedProductPhotoTarget.group !== "ai-white-background"
) {
const matchedGroupFile =
productPhotoWorkspaceData.groupFiles[
selectedProductPhotoTarget.group
@ -56,6 +59,7 @@ export function useProductPhotoTargetSync({
fileId: "",
};
} else if (
selectedProductPhotoTarget.group === "nine-view" &&
PRODUCT_PHOTO_NINE_VIEW_SLOTS.some(
(slot) => slot.key === selectedProductPhotoTarget.slot,
)

View File

@ -38,7 +38,6 @@ export function DocumentWorkspaceHeaderActionsSlot({
activeProductPhotoPanel,
canDeleteSelectedReviewRound,
canDownloadWorkspaceFiles,
canUseAiWhiteBackground,
canUseProductPricingPushControls,
confirmedClassName,
currentProductPhotoGroupUploadLabel,
@ -116,7 +115,6 @@ export function DocumentWorkspaceHeaderActionsSlot({
onExportReviewSheet,
onFinishedSampleShipmentStatusClick,
onImportProductLimitWorkbookChange,
onOpenAiWhiteBackground,
onOpenProductPhotoCurrentUpload,
onOpenProductPhotoPanelUpload,
onProductFactoryReopenFieldChange,
@ -143,7 +141,6 @@ export function DocumentWorkspaceHeaderActionsSlot({
activeProductPhotoPanel: ProductPhotoPanel;
canDeleteSelectedReviewRound: boolean;
canDownloadWorkspaceFiles: boolean;
canUseAiWhiteBackground: boolean;
canUseProductPricingPushControls: boolean;
confirmedClassName: string;
currentProductPhotoGroupUploadLabel: string;
@ -223,7 +220,6 @@ export function DocumentWorkspaceHeaderActionsSlot({
onImportProductLimitWorkbookChange: (
event: ChangeEvent<HTMLInputElement>,
) => void;
onOpenAiWhiteBackground: () => void;
onOpenProductPhotoCurrentUpload: (target: ProductPhotoSelection) => void;
onOpenProductPhotoPanelUpload: (panel: ProductPhotoPanel) => void;
onProductFactoryReopenFieldChange: (value: ProductFactoryReopenField) => void;
@ -351,7 +347,6 @@ export function DocumentWorkspaceHeaderActionsSlot({
<ProductPhotoHeaderActions
actionGroupClassName={actionGroupClassName}
isReadOnly={isReadOnly}
canUseAiWhiteBackground={canUseAiWhiteBackground}
canDownloadWorkspaceFiles={canDownloadWorkspaceFiles}
detailUploadInputRef={productPhotoDetailUploadInputRef}
currentUploadInputRef={productPhotoCurrentUploadInputRef}
@ -365,7 +360,6 @@ export function DocumentWorkspaceHeaderActionsSlot({
downloadFiles={activeProductPhotoDownloadFiles}
selectedFile={effectiveSelectedWorkspaceFile}
confirmationAction={documentWorkspaceConfirmationAction}
onOpenAiWhiteBackground={onOpenAiWhiteBackground}
onOpenPanelUpload={onOpenProductPhotoPanelUpload}
onOpenCurrentUpload={onOpenProductPhotoCurrentUpload}
onPanelUploadChange={onUploadProductPhotoPanelFiles}