feat: 调用gpt\nano模型方式修改,功能优化,增加下载采集图片等功能
This commit is contained in:
@@ -13,9 +13,10 @@ import { createRoot } from 'react-dom/client';
|
||||
import { App as AntApp, ConfigProvider, Popover, Progress, Select } from 'antd';
|
||||
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import type { ScanResult, ImageMaterial } from '../../src/collector/scan';
|
||||
import { cleanFilename } from '../../src/collector/url';
|
||||
import {
|
||||
buildGeneratePayload, suiteZipUrl, uploadImage,
|
||||
DEFAULT_PLAN, IMAGE_MODEL_OPTIONS, PLATFORM_OPTIONS, PLATFORM_SPECS, STYLE_SET_OPTIONS, WEAK_FIDELITY_MODELS,
|
||||
buildGeneratePayload, downloadSuiteZip, uploadImage, exportImages,
|
||||
DEFAULT_PLAN, IMAGE_MODEL_OPTIONS, PLATFORM_OPTIONS, PLATFORM_SPECS, STYLE_SET_OPTIONS,
|
||||
type PlanItem, type SuiteInfo,
|
||||
} from '../../src/api/client';
|
||||
import { loadSettings, saveSettings, type BackendSettings } from '../../src/storage/settings';
|
||||
@@ -141,8 +142,8 @@ const App: React.FC = () => {
|
||||
// 出图方案
|
||||
const [platform, setPlatform] = useState<'ozon' | 'wb' | 'cn'>('ozon');
|
||||
const [styleSet, setStyleSet] = useState(1);
|
||||
/** 生图模型(默认 gpt-image-2,走 RightAPI provider) */
|
||||
const [model, setModel] = useState<string>('gpt-image-2');
|
||||
/** 生图模型(默认 gpt-image-2-vip,走 RightAPI provider) */
|
||||
const [model, setModel] = useState<string>('gpt-image-2-vip');
|
||||
/** 用户改写的风格提示词(按风格 id 存,切风格不丢) */
|
||||
const [stylePrompts, setStylePrompts] = useState<Record<number, string>>({});
|
||||
/** 生图要求(最高优先级,强制约束,覆盖其他设定) */
|
||||
@@ -156,6 +157,10 @@ const App: React.FC = () => {
|
||||
// 生成
|
||||
const [suite, setSuite] = useState<SuiteInfo | null>(null);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
// 导出采集图片
|
||||
const [exporting, setExporting] = useState(false);
|
||||
// 导出生成结果 ZIP
|
||||
const [exportingZip, setExportingZip] = useState(false);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
/** 规划请求序号:重新采集时递增,用于丢弃重置后才返回的过期规划响应 */
|
||||
const planSeqRef = useRef(0);
|
||||
@@ -438,12 +443,6 @@ const App: React.FC = () => {
|
||||
{STYLE_SET_OPTIONS.find(s => s.value === styleSet)?.label}」,共 {totalPlanned} 张、参考图{' '}
|
||||
{selectedKeys.size} 张。生成需要几分钟,可在下方查看进度。
|
||||
</div>
|
||||
{WEAK_FIDELITY_MODELS.has(model) && (
|
||||
<div style={{ marginTop: 8, color: '#d4380d', fontWeight: 600 }}>
|
||||
⚠️ 「{model}」为官逆通道,商品还原度不稳定,可能生成与原商品不符的图片。建议先只出 1
|
||||
张确认效果,正式套图请改用 gpt-image-2。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
okText: '开始生成', cancelText: '取消',
|
||||
@@ -451,9 +450,27 @@ const App: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const handleExport = async () => {
|
||||
if (!suite) return;
|
||||
chrome.tabs.create({ url: suiteZipUrl(settings.baseUrl, suite.id) });
|
||||
setExportingZip(true);
|
||||
try {
|
||||
const blob = await downloadSuiteZip(settings.baseUrl, settings.token, suite.id);
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
await chrome.downloads.download({
|
||||
url: objectUrl,
|
||||
filename: `${cleanFilename(productTitle) || '套图'}.zip`,
|
||||
saveAs: false,
|
||||
});
|
||||
} finally {
|
||||
// 延迟释放,给下载任务足够时间读取 blob
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
|
||||
}
|
||||
} catch (e) {
|
||||
modal.error({ title: '导出失败', content: e instanceof Error ? e.message : String(e), okText: '知道了' });
|
||||
} finally {
|
||||
setExportingZip(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleKey = (key: string) => {
|
||||
@@ -465,9 +482,50 @@ const App: React.FC = () => {
|
||||
/** 全部可选图片:采集结果 + 手动上传(上传图单独一组「upload」) */
|
||||
const allImages: ImageMaterial[] = result ? [...result.images, ...uploadedImages] : uploadedImages;
|
||||
|
||||
/** 导出文件名的商品标题(编辑态优先,回退采集标题) */
|
||||
const productTitle = titleEdit.trim()
|
||||
|| result?.texts.find(t => t.kind === 'title')?.content?.trim()
|
||||
|| '';
|
||||
|
||||
const groupImages = (groupKey: string): ImageMaterial[] =>
|
||||
allImages.filter(i => i.groupKey === groupKey);
|
||||
|
||||
/** 下载采集图片:只打包已勾选的图片(后端按分组名建文件夹),前端经 chrome.downloads 落盘 */
|
||||
const handleExportImages = async () => {
|
||||
const imgs = allImages.filter(i => i.type === 'img' && selectedKeys.has(i.key));
|
||||
if (imgs.length === 0) {
|
||||
modal.warning({ title: '请先勾选要下载的图片' });
|
||||
return;
|
||||
}
|
||||
setExporting(true);
|
||||
try {
|
||||
const blob = await exportImages(settings.baseUrl, settings.token, {
|
||||
title: productTitle,
|
||||
images: imgs.map(i => ({
|
||||
url: i.url,
|
||||
groupName: i.groupName,
|
||||
variantName: i.variantName ?? null,
|
||||
key: i.key,
|
||||
})),
|
||||
});
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
await chrome.downloads.download({
|
||||
url: objectUrl,
|
||||
filename: `${cleanFilename(productTitle) || '采集图片'}.zip`,
|
||||
saveAs: false,
|
||||
});
|
||||
} finally {
|
||||
// 延迟释放,给下载任务足够时间读取 blob
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
|
||||
}
|
||||
} catch (e) {
|
||||
modal.error({ title: '导出失败', content: e instanceof Error ? e.message : String(e), okText: '知道了' });
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 预览用的全量图序列(主图→SKU→详情→上传,与展示顺序一致) */
|
||||
const collectedPreviewList: string[] = (['main', 'sku', 'detail', 'upload'] as const)
|
||||
.flatMap(g => groupImages(g).map(i => i.url));
|
||||
@@ -484,6 +542,19 @@ const App: React.FC = () => {
|
||||
setPlan(prev => prev.map((p, i) => i === idx ? { ...p, count } : p));
|
||||
};
|
||||
|
||||
/** 全部方案是否都启用(任一为 0 即视为未全选,复选框随方案状态联动) */
|
||||
const planAllEnabled = plan.every(p => p.count >= 1);
|
||||
|
||||
/** 批量开关:勾选 → 全部至少 1 张(用户已设 >1 的保留原数量);取消勾选 → 全部 0,方便单独勾选一种方案 */
|
||||
const togglePlanAll = (on: boolean) => {
|
||||
setPlan(prev => prev.map(p => on ? { ...p, count: Math.max(p.count, 1) } : { ...p, count: 0 }));
|
||||
};
|
||||
|
||||
/** 点击方案行:勾选(0 → 1)/ 取消勾选(>0 → 0),配合"全部方案"开关单独选一种 */
|
||||
const togglePlanRow = (idx: number) => {
|
||||
setPlan(prev => prev.map((p, i) => i === idx ? { ...p, count: p.count > 0 ? 0 : 1 } : p));
|
||||
};
|
||||
|
||||
/** 手动上传:点击触发隐藏的 file input */
|
||||
const handleUpload = () => fileRef.current?.click();
|
||||
|
||||
@@ -739,6 +810,14 @@ const App: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{allImages.some(i => i.type === 'img') && (
|
||||
<div className="img-export-bar">
|
||||
<button className="btn btn-sm" onClick={handleExportImages} disabled={exporting}>
|
||||
<DownloadOutlined /> {exporting ? '下载中…' : '下载采集图片'}
|
||||
</button>
|
||||
<span className="hint">下载已勾选图片({allImages.filter(i => i.type === 'img' && selectedKeys.has(i.key)).length} 张)</span>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -765,17 +844,35 @@ const App: React.FC = () => {
|
||||
>
|
||||
<div className="plan-list">
|
||||
{plan.map((p, idx) => (
|
||||
<div key={idx} className={`plan-row ${p.count === 0 ? 'off' : ''}`}>
|
||||
<div
|
||||
key={idx}
|
||||
className={`plan-row ${p.count === 0 ? 'off' : ''}`}
|
||||
title="点击勾选/取消该方案(数量用右侧 ± 调整)"
|
||||
onClick={() => togglePlanRow(idx)}
|
||||
>
|
||||
<div className="plan-main">
|
||||
<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)} />
|
||||
<span onClick={(e) => e.stopPropagation()}>
|
||||
<Stepper value={p.count} onChange={(v) => setPlanCount(idx, v)} />
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="plan-all-toggle">
|
||||
<label className="auto-chk" title="勾选:全部方案至少 1 张(>1 张保留原数量);取消勾选:全部改为 0,方便单独勾选一种方案">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={planAllEnabled}
|
||||
onChange={(e) => togglePlanAll(e.target.checked)}
|
||||
/>
|
||||
全部方案
|
||||
</label>
|
||||
<span className="hint">取消勾选后全部为 0,点选需要的方案行即可单独出图</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, margin: '10px 0 4px' }}>
|
||||
{planSource === 'ai' && (
|
||||
<button
|
||||
@@ -875,8 +972,8 @@ const App: React.FC = () => {
|
||||
title="生成结果"
|
||||
extra={
|
||||
suite && ['done', 'partial'].includes(suite.status) && (
|
||||
<button className="btn btn-sm" onClick={handleExport}>
|
||||
<DownloadOutlined /> 导出 ZIP
|
||||
<button className="btn btn-sm" onClick={handleExport} disabled={exportingZip}>
|
||||
<DownloadOutlined /> {exportingZip ? '导出中…' : '导出 ZIP'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user