feat: 调用gpt\nano模型方式修改,功能优化,增加下载采集图片等功能

This commit is contained in:
R524809
2026-08-20 17:00:40 +08:00
parent 90b7c8737d
commit 95cb93a160
20 changed files with 411 additions and 407 deletions
+113 -16
View File
@@ -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>
)
}