feat: deepseek 的一些修改
This commit is contained in:
@@ -11,10 +11,10 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App as AntApp, ConfigProvider, Popover, Progress, Select } from 'antd';
|
||||
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import type { ScanResult, ImageMaterial } from '../../src/collector/scan';
|
||||
import {
|
||||
buildGeneratePayload, suiteZipUrl,
|
||||
buildGeneratePayload, suiteZipUrl, uploadImage,
|
||||
DEFAULT_PLAN, IMAGE_MODEL_OPTIONS, PLATFORM_OPTIONS, PLATFORM_SPECS, STYLE_SET_OPTIONS,
|
||||
type PlanItem, type SuiteInfo,
|
||||
} from '../../src/api/client';
|
||||
@@ -121,6 +121,12 @@ const App: React.FC = () => {
|
||||
const [descEdit, setDescEdit] = useState('');
|
||||
const [paramsOpen, setParamsOpen] = useState(false);
|
||||
|
||||
// 手动上传图片(补充参考图,独立「upload」分组)
|
||||
const [uploadedImages, setUploadedImages] = useState<ImageMaterial[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileRef = useRef<HTMLInputElement | null>(null);
|
||||
const uploadSeqRef = useRef(0);
|
||||
|
||||
// 服务端
|
||||
const [settings, setSettings] = useState<BackendSettings>({ baseUrl: 'http://127.0.0.1:3300', token: '' });
|
||||
|
||||
@@ -131,6 +137,8 @@ const App: React.FC = () => {
|
||||
const [model, setModel] = useState<string>('wan2.7-image-pro');
|
||||
/** 用户改写的风格提示词(按风格 id 存,切风格不丢) */
|
||||
const [stylePrompts, setStylePrompts] = useState<Record<number, string>>({});
|
||||
/** 生图要求(最高优先级,强制约束,覆盖其他设定) */
|
||||
const [requirements, setRequirements] = useState('');
|
||||
const [plan, setPlan] = useState<PlanItem[]>(DEFAULT_PLAN.map(p => ({ ...p })));
|
||||
const [planSource, setPlanSource] = useState<'default' | 'ai'>('default');
|
||||
const [planSummary, setPlanSummary] = useState('');
|
||||
@@ -261,7 +269,7 @@ const App: React.FC = () => {
|
||||
if (['done', 'partial', 'failed'].includes(s.status)) {
|
||||
stopPolling();
|
||||
setGenerating(false);
|
||||
if (s.status === 'partial') modal.warning({ title: '部分生成失败', content: '可重试或更换风格重新生成', okText: '知道了' });
|
||||
if (s.status === 'partial') modal.warning({ title: '部分生成失败', content: s.error || '可重试或更换风格重新生成', okText: '知道了' });
|
||||
if (s.status === 'failed') modal.error({ title: '生成失败', content: s.error || '未知错误', okText: '知道了' });
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -314,8 +322,8 @@ const App: React.FC = () => {
|
||||
setSuite(null);
|
||||
try {
|
||||
const payload = buildGeneratePayload(
|
||||
result, selectedKeys, editedTexts(),
|
||||
{ style_set: styleSet, style_prompt: currentStylePrompt, plan: activePlan, platform, model },
|
||||
allImages, selectedKeys, editedTexts(),
|
||||
{ style_set: styleSet, style_prompt: currentStylePrompt, requirements: requirements.trim() || null, plan: activePlan, platform, model },
|
||||
);
|
||||
const { suite_id } = await send<{ suite_id: string }>('generateSuite', {
|
||||
baseUrl: settings.baseUrl, token: settings.token, payload,
|
||||
@@ -343,6 +351,7 @@ const App: React.FC = () => {
|
||||
sku_variants: skuVariants,
|
||||
image_stats: result.stats,
|
||||
platform,
|
||||
requirements: requirements.trim() || null,
|
||||
},
|
||||
});
|
||||
if (seq !== planSeqRef.current) return; // 规划已被重新采集重置,丢弃过期响应
|
||||
@@ -398,13 +407,15 @@ const App: React.FC = () => {
|
||||
setSelectedKeys(next);
|
||||
};
|
||||
|
||||
const groupImages = (groupKey: string): ImageMaterial[] =>
|
||||
result?.images.filter(i => i.groupKey === groupKey) ?? [];
|
||||
/** 全部可选图片:采集结果 + 手动上传(上传图单独一组「upload」) */
|
||||
const allImages: ImageMaterial[] = result ? [...result.images, ...uploadedImages] : uploadedImages;
|
||||
|
||||
/** 预览用的全量图序列(主图→SKU→详情,与展示顺序一致) */
|
||||
const collectedPreviewList: string[] = result
|
||||
? (['main', 'sku', 'detail'] as const).flatMap(g => groupImages(g).map(i => i.url))
|
||||
: [];
|
||||
const groupImages = (groupKey: string): ImageMaterial[] =>
|
||||
allImages.filter(i => i.groupKey === groupKey);
|
||||
|
||||
/** 预览用的全量图序列(主图→SKU→详情→上传,与展示顺序一致) */
|
||||
const collectedPreviewList: string[] = (['main', 'sku', 'detail', 'upload'] as const)
|
||||
.flatMap(g => groupImages(g).map(i => i.url));
|
||||
/** 生成结果的预览序列(仅成功的图) */
|
||||
const resultPreviewList: string[] = suite ? suite.images.filter(i => i.status === 'ok').map(i => i.url) : [];
|
||||
|
||||
@@ -418,6 +429,43 @@ const App: React.FC = () => {
|
||||
setPlan(prev => prev.map((p, i) => i === idx ? { ...p, count } : p));
|
||||
};
|
||||
|
||||
/** 手动上传:点击触发隐藏的 file input */
|
||||
const handleUpload = () => fileRef.current?.click();
|
||||
|
||||
const onFilesChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files ?? []);
|
||||
e.target.value = ''; // 清空以便再次选择同名文件
|
||||
if (files.length === 0) return;
|
||||
setUploading(true);
|
||||
const added: ImageMaterial[] = [];
|
||||
for (const f of files) {
|
||||
try {
|
||||
const { url } = await uploadImage(settings.baseUrl, settings.token, f);
|
||||
added.push({
|
||||
key: `upload-${String(++uploadSeqRef.current).padStart(3, '0')}`,
|
||||
groupKey: 'upload',
|
||||
groupName: '手动上传',
|
||||
url,
|
||||
thumbUrl: url,
|
||||
index: added.length,
|
||||
type: 'img',
|
||||
});
|
||||
} catch (err) {
|
||||
modal.error({ title: '上传失败', content: `${f.name}:${err instanceof Error ? err.message : String(err)}`, okText: '知道了' });
|
||||
}
|
||||
}
|
||||
if (added.length > 0) {
|
||||
setUploadedImages(prev => [...prev, ...added]);
|
||||
setSelectedKeys(prev => {
|
||||
const next = new Set(prev);
|
||||
added.forEach(a => next.add(a.key));
|
||||
return next;
|
||||
});
|
||||
modal.success({ title: `已上传 ${added.length} 张图片` });
|
||||
}
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
/** 源站图防盗链时的兜底:走服务端图片代理 */
|
||||
const proxied = (u: string) =>
|
||||
`${settings.baseUrl.replace(/\/$/, '')}/api/proxy-image?url=${encodeURIComponent(u)}`;
|
||||
@@ -567,17 +615,34 @@ const App: React.FC = () => {
|
||||
no="02"
|
||||
title="采集图片"
|
||||
className="section-images"
|
||||
extra={result ? `已选 ${selectedKeys.size} / ${result.images.filter(i => i.type !== 'video').length}` : undefined}
|
||||
extra={
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
{(result || uploadedImages.length > 0) && (
|
||||
<span>已选 {selectedKeys.size} / {allImages.filter(i => i.type !== 'video').length}</span>
|
||||
)}
|
||||
<button className="btn btn-sm" onClick={handleUpload} disabled={uploading}>
|
||||
<UploadOutlined /> {uploading ? '上传中…' : '上传图片'}
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{!result ? (
|
||||
<div className="empty">采集后在此勾选图片</div>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={onFilesChange}
|
||||
/>
|
||||
{allImages.length === 0 ? (
|
||||
<div className="empty">采集后在此勾选图片,或点右上角「上传图片」手动补充</div>
|
||||
) : (
|
||||
<div className="img-groups">
|
||||
{['main', 'sku', 'detail'].map(g => groupImages(g).length > 0 && (
|
||||
{(['main', 'sku', 'detail', 'upload'] as const).map(g => groupImages(g).length > 0 && (
|
||||
<div key={g} style={{ marginBottom: 10 }}>
|
||||
<div className="group-head">
|
||||
<span className="name">
|
||||
{g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : '详情图'}
|
||||
{g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : g === 'detail' ? '详情图' : '手动上传'}
|
||||
</span>
|
||||
<span className="count">{groupImages(g).length}</span>
|
||||
<span
|
||||
@@ -647,6 +712,7 @@ const App: React.FC = () => {
|
||||
<span className="plan-title">{p.title}</span>
|
||||
{p.variant_name && <span className="variant-chip">{p.variant_name}</span>}
|
||||
{p.detail && <span className="plan-detail" title={p.detail}>{p.detail}</span>}
|
||||
{p.prompt_hint && <span className="plan-detail plan-hint" title={p.prompt_hint}>🎯 {p.prompt_hint}</span>}
|
||||
</div>
|
||||
<Stepper value={p.count} onChange={(v) => setPlanCount(idx, v)} />
|
||||
</div>
|
||||
@@ -698,6 +764,15 @@ const App: React.FC = () => {
|
||||
onChange={(e) => setStylePrompts(p => ({ ...p, [styleSet]: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>生图要求(优先级最高,强制要求,会覆盖其他设定)</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={requirements}
|
||||
onChange={(e) => setRequirements(e.target.value)}
|
||||
placeholder="选填,例如:必须保留商品正面品牌标识;背景必须为纯黑色;不得添加任何文字水印"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 4 }}>
|
||||
{generating && (
|
||||
<div style={{ flex: 1 }}>
|
||||
@@ -773,7 +848,9 @@ const App: React.FC = () => {
|
||||
<div style={{ aspectRatio: suite.ratio === '3:4' ? '3/4' : '1', background: 'var(--card-soft)' }} />
|
||||
)}
|
||||
{img.status !== 'ok' && <span className="fail-tag">{img.status === 'failed' ? '✗' : '…'}</span>}
|
||||
<div className="cap">{img.name}</div>
|
||||
<div className="cap" title={img.error || img.name}>
|
||||
{img.status === 'failed' && img.error ? img.error : img.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -275,6 +275,8 @@
|
||||
font-size: 12px; color: var(--text-2);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
/* 构图提示(prompt_hint):生图要求的落地处,用主题蓝区分 */
|
||||
.plan-detail.plan-hint { color: #4f6bed; }
|
||||
.stepper { display: inline-flex; align-items: center; gap: 0; flex-shrink: 0; }
|
||||
.step-btn {
|
||||
width: 24px; height: 24px; border: 1px solid var(--border-strong); background: #fff;
|
||||
|
||||
Reference in New Issue
Block a user