feat: 生图增加商品尺寸填报
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }}>
|
||||
生图要求(优先级最高,强制约束,会覆盖其他设定)
|
||||
|
||||
@@ -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) 查询进度。 */
|
||||
|
||||
Reference in New Issue
Block a user