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>
)
}
+17 -4
View File
@@ -14,6 +14,7 @@
--primary: #8b5cf6; /* 紫(ozon-seller-kit v2 主题色) */
--primary-hover: #7c3aed;
--primary-ring: rgba(139, 92, 246, 0.12);
--primary-soft: #a78bfa; /* 主题色同色系偏淡(未勾选描边/✓) */
--green: #52c41a;
--red: #ff4d4f;
--warn-bg: #fffbe6;
@@ -49,6 +50,12 @@
.section-images .section-head { padding: 0 16px; }
.section-images .img-groups { padding: 2px 8px 0 16px; }
.section-images .empty { margin: 0 16px; }
.img-export-bar {
display: flex; align-items: center; gap: 10px;
margin: 10px 16px 2px; padding-top: 10px;
border-top: 1px solid var(--border);
}
.img-export-bar .hint { font-size: 12px; color: var(--text-2); }
.divider { border-top: 1px solid var(--border); margin: 12px 0; }
/* ── 顶部 ── */
@@ -174,11 +181,11 @@
.img-cell.on { border-color: var(--primary); }
.img-cell .tick {
position: absolute; top: 5px; left: 5px; width: 18px; height: 18px;
border-radius: 50%; border: 1.5px solid #fff;
border-radius: 50%; border: 1.5px solid var(--primary-soft);
background: rgba(255, 255, 255, 0.55); display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 11px; transition: all .15s; cursor: pointer;
color: var(--primary-soft); font-size: 11px; transition: all .15s; cursor: pointer;
}
.img-cell.on .tick { background: var(--primary); border-color: var(--primary); }
.img-cell.on .tick { background: var(--primary); border-color: var(--primary); color: #fff; }
.img-cell .variant {
position: absolute; bottom: 0; left: 0; right: 0;
background: rgba(0, 0, 0, 0.55); color: #fff; font-size: 11px;
@@ -262,9 +269,15 @@
.plan-row {
display: flex; align-items: center; gap: 10px;
padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px;
background: var(--card-soft);
background: var(--card-soft); cursor: pointer;
}
.plan-row:hover { border-color: var(--primary); }
.plan-row.off { opacity: .45; }
.plan-all-toggle {
display: flex; align-items: center; gap: 10px;
margin: 8px 2px 2px; padding-top: 6px;
border-top: 1px dashed var(--border);
}
.plan-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
.plan-title { font-size: 13.5px; font-weight: 600; white-space: nowrap; }
.variant-chip {