feat: 生图增加商品尺寸填报

This commit is contained in:
R524809
2026-09-11 17:28:06 +08:00
parent 27c5daed72
commit dbb2bb80bd
8 changed files with 188 additions and 35 deletions
+55 -6
View File
@@ -1,8 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { Alert, Button, Image, Input, message, Select, Space, Typography } from 'antd';
import { Fragment, useEffect, useRef, useState } from 'react';
import { Alert, Button, Checkbox, Image, Input, InputNumber, message, Select, Space, Typography } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { apiErrorMessage } from '@/services/api';
import { DEFAULT_IMAGE_MODEL, IMAGE_MODEL_OPTIONS, cancelSuite, getSuite, imageEditSingle, WatermarkPayload } from '@/services/suite';
import { DEFAULT_IMAGE_MODEL, IMAGE_MODEL_OPTIONS, cancelSuite, getSuite, imageEditSingle, ProductDimensions, WatermarkPayload } from '@/services/suite';
const { Text } = Typography;
@@ -27,6 +27,8 @@ interface Props {
export default function AiImageGenModal({ open, productId, source, onClose, onGenerated, onSubmitted, watermark }: Props) {
const [prompt, setPrompt] = useState('');
const [model, setModel] = useState<string>(DEFAULT_IMAGE_MODEL);
const [dimensions, setDimensions] = useState<ProductDimensions>({ unit: 'mm' });
const [generateSizeChart, setGenerateSizeChart] = useState(false);
const [generating, setGenerating] = useState(false);
const [resultUrl, setResultUrl] = useState('');
const [taskId, setTaskId] = useState('');
@@ -48,6 +50,8 @@ export default function AiImageGenModal({ open, productId, source, onClose, onGe
useEffect(() => {
if (open && !activeTaskRef.current) {
setPrompt('');
setDimensions({ unit: 'mm' });
setGenerateSizeChart(false);
setResultUrl('');
setGenerating(false);
setTaskId('');
@@ -126,6 +130,12 @@ export default function AiImageGenModal({ open, productId, source, onClose, onGe
const onGenerate = async () => {
if (!source) return;
const hasCompleteDimensions = [dimensions.length, dimensions.width, dimensions.height]
.every((value) => typeof value === 'number' && value > 0);
if (generateSizeChart && !hasCompleteDimensions) {
message.warning('生成尺寸标注图前,请完整填写商品长、宽、高');
return;
}
setGenerating(true);
setResultUrl('');
setTaskError('');
@@ -138,6 +148,8 @@ export default function AiImageGenModal({ open, productId, source, onClose, onGe
append: true,
after_asset_id: source.afterAssetId,
watermark: watermark?.enabled ? watermark : undefined,
generate_size_chart: generateSizeChart,
dimensions: hasCompleteDimensions ? dimensions : null,
});
setTaskId(r.suite_id);
message.info('已提交后台生成,可继续操作;进度将在「生成结果」区域显示');
@@ -209,6 +221,33 @@ export default function AiImageGenModal({ open, productId, source, onClose, onGe
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ display: 'block', fontSize: 12, marginBottom: 4 }}>商品尺寸(长 × 宽 × 高)</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{(['length', 'width', 'height'] as const).map((key, index) => (
<Fragment key={key}>
<InputNumber
min={0}
precision={2}
controls={false}
style={{ width: '100%' }}
value={dimensions[key]}
onChange={(value) => setDimensions((prev) => ({ ...prev, [key]: typeof value === 'number' ? value : undefined }))}
placeholder={['长', '宽', '高'][index]}
disabled={generating}
/>
{index < 2 && <Text type="secondary" style={{ fontSize: 16 }}>×</Text>}
</Fragment>
))}
<Select
style={{ width: 76, flexShrink: 0 }}
value={dimensions.unit ?? 'mm'}
onChange={(unit: 'mm' | 'cm') => setDimensions((prev) => ({ ...prev, unit }))}
options={[{ value: 'mm', label: 'mm' }, { value: 'cm', label: 'cm' }]}
disabled={generating}
/>
</div>
</div>
<Input.TextArea
rows={5}
value={prompt}
@@ -234,9 +273,19 @@ export default function AiImageGenModal({ open, productId, source, onClose, onGe
</div>
)}
/>
<Button type="primary" style={{ width: 112 }} loading={generating} onClick={onGenerate}>
提交任务
</Button>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: 4, flexShrink: 0 }}>
<Checkbox
checked={generateSizeChart}
onChange={(e) => setGenerateSizeChart(e.target.checked)}
disabled={generating}
style={{ whiteSpace: 'nowrap' }}
>
生成尺寸标注图
</Checkbox>
<Button type="primary" loading={generating} onClick={onGenerate}>
提交任务
</Button>
</div>
</div>
</div>
<Alert
+38 -3
View File
@@ -3,9 +3,9 @@
* 采集图片(分组勾选/上传/单张AI生图) + 出图方案(AI规划/风格/要求/模型/一键生成) + 生成结果(导出 ZIP)。
* 交互对齐 image-suite-studio 面板 02/03/04 区块;套图服务端接口 Phase B 提供。
*/
import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { Fragment, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import {
Alert, App as AntdApp, Button, Card, Checkbox, Col, Empty, Input, Popconfirm, Popover, Progress,
Alert, App as AntdApp, Button, Card, Checkbox, Col, Empty, Input, InputNumber, Popconfirm, Popover, Progress,
Row, Segmented, Select, Space, Tag, Typography, Upload,
} from 'antd';
import { DownloadOutlined, SettingOutlined, ThunderboltOutlined, UploadOutlined } from '@ant-design/icons';
@@ -15,7 +15,7 @@ import {
DEFAULT_IMAGE_MODEL, DEFAULT_PLAN, DEFAULT_WATERMARK, IMAGE_MODEL_OPTIONS,
STYLE_SET_OPTIONS, SuiteInfo, SuiteTextPayload, WatermarkPayload,
applyWatermarkToAssets, cancelSuite, deleteAsset, exportImage, exportImages, generateSuite, getSuite, listProductSuites, listProductWatermarkJobs, planSuite, removeWatermarkFromAssets, retrySuite, retrySuiteItem, uploadProductAsset,
type PlanItem,
type PlanItem, type ProductDimensions,
} from '@/services/suite';
import { cleanFilename, downloadBlob } from '@/utils/file';
import AiImageGenModal from './AiImageGenModal';
@@ -28,6 +28,9 @@ const { Text } = Typography;
const WATERMARK_STORAGE_KEY = 'trialWatermark';
const hasCompleteDimensions = (dimensions: ProductDimensions) =>
[dimensions.length, dimensions.width, dimensions.height].every((value) => typeof value === 'number' && value > 0);
/** 方案数量步进器:左减号 / 中间可输入 / 右加号(0-5) */
function PlanStepper({ value, onChange }: { value: number; onChange: (v: number) => void }) {
const [text, setText] = useState(String(value));
@@ -158,6 +161,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
const [styleSet, setStyleSet] = useState(1);
const [stylePrompts, setStylePrompts] = useState<Record<number, string>>({});
const [requirements, setRequirements] = useState('');
const [dimensions, setDimensions] = useState<ProductDimensions>({ unit: 'mm' });
const [model, setModel] = useState<string>(DEFAULT_IMAGE_MODEL);
const [watermark, setWatermark] = useState<WatermarkPayload>(loadWatermark);
@@ -531,6 +535,10 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
message.warning('出图方案的张数都是 0');
return;
}
if (activePlan.some((item) => item.kind === 'size_chart') && !hasCompleteDimensions(dimensions)) {
message.warning('方案包含尺寸标注图,请先在右侧完整填写商品长、宽、高及单位,或将该项数量设为 0');
return;
}
setGenerating(true);
setSuite(null);
try {
@@ -543,6 +551,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
style_set: styleSet,
style_prompt: currentStylePrompt || null,
requirements: requirements.trim() || null,
dimensions: hasCompleteDimensions(dimensions) ? dimensions : null,
plan: activePlan,
platform: 'ozon',
model,
@@ -641,6 +650,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
image_stats: stats,
platform: 'ozon',
requirements: requirements.trim() || null,
dimensions: hasCompleteDimensions(dimensions) ? dimensions : null,
});
if (seq !== planSeqRef.current) return; // 已被重置,丢弃过期响应
setPlan(data.items);
@@ -1133,6 +1143,31 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
onChange={(e) => setStylePrompts((p) => ({ ...p, [styleSet]: e.target.value }))}
/>
</div>
<div style={{ marginBottom: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}>商品尺寸(长 × 宽 × 高)</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4 }}>
{(['length', 'width', 'height'] as const).map((key, index) => (
<Fragment key={key}>
<InputNumber
min={0}
precision={2}
controls={false}
style={{ width: '100%' }}
value={dimensions[key]}
onChange={(value) => setDimensions((prev) => ({ ...prev, [key]: typeof value === 'number' ? value : undefined }))}
placeholder={['长', '宽', '高'][index]}
/>
{index < 2 && <Text type="secondary" style={{ fontSize: 16 }}>×</Text>}
</Fragment>
))}
<Select
style={{ width: 76, flexShrink: 0 }}
value={dimensions.unit ?? 'mm'}
onChange={(unit: 'mm' | 'cm') => setDimensions((prev) => ({ ...prev, unit }))}
options={[{ value: 'mm', label: 'mm' }, { value: 'cm', label: 'cm' }]}
/>
</div>
</div>
<div>
<Text type="secondary" style={{ fontSize: 12 }}>
生图要求(优先级最高,强制约束,会覆盖其他设定)
+17 -1
View File
@@ -32,7 +32,10 @@ export interface PlanItem {
}
/** 默认方案:7 种基础类型各 1 张(AI 规划前) */
export const DEFAULT_PLAN: PlanItem[] = SUITE_TYPE_OPTIONS.slice(0, 7).map((t) => ({
export const DEFAULT_PLAN: PlanItem[] = SUITE_TYPE_OPTIONS
.filter((t) => t.value !== 'multi_scene')
.slice(0, 7)
.map((t) => ({
kind: t.value,
title: t.label,
detail: '',
@@ -41,6 +44,14 @@ export const DEFAULT_PLAN: PlanItem[] = SUITE_TYPE_OPTIONS.slice(0, 7).map((t) =
variant_name: null,
}));
/** 商品外廓尺寸;单位可选 mm 或 cm。 */
export interface ProductDimensions {
length?: number;
width?: number;
height?: number;
unit?: 'mm' | 'cm';
}
/** 视觉风格(默认提示词可在页面上改写,随生成请求覆盖后端模板) */
export const STYLE_SET_OPTIONS = [
{ value: 1, label: '北欧极简', prompt: 'Nordic minimalism: light gray or warm off-white background, soft diffused light, low-saturation palette, generous negative space, restrained and clean composition' },
@@ -100,6 +111,7 @@ export interface SuitePlanPayload {
image_stats: Record<string, number>;
platform: string;
requirements?: string | null;
dimensions?: ProductDimensions | null;
}
export interface SuiteGeneratePayload {
@@ -113,6 +125,7 @@ export interface SuiteGeneratePayload {
platform: string;
model?: string | null;
watermark?: WatermarkPayload;
dimensions?: ProductDimensions | null;
}
export interface SuiteImageInfo {
@@ -193,6 +206,9 @@ export interface ImageEditSinglePayload {
after_asset_id?: string;
/** 单张生成完成后由服务端合成水印;关闭时不传。 */
watermark?: WatermarkPayload;
/** 勾选时将当前图按尺寸标注图任务处理。 */
generate_size_chart?: boolean;
dimensions?: ProductDimensions | null;
}
/** ★ 提交单张 AI 生图任务;通过 getSuite(suite_id) 查询进度。 */