feat: AI 生图、图片下载优化
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, type Key } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button, Card, Input, message, Popconfirm, Space, Table, Tag, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
@@ -43,6 +43,7 @@ export default function CollectionPage() {
|
||||
const res = await listProducts({ stage, q: q || undefined, page, page_size: pageSize });
|
||||
setItems(res.items);
|
||||
setTotal(res.total);
|
||||
setSelectedKeys([]);
|
||||
} catch (e) {
|
||||
message.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
@@ -76,6 +77,26 @@ export default function CollectionPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 勾选与当前列表视图绑定:每次 load 后清空,避免残留不可见商品的勾选
|
||||
const [selectedKeys, setSelectedKeys] = useState<Key[]>([]);
|
||||
const allSelected = items.length > 0 && selectedKeys.length === items.length;
|
||||
const toggleSelectAll = () => {
|
||||
setSelectedKeys(allSelected ? [] : items.map((i) => i.id));
|
||||
};
|
||||
|
||||
const onBatchDelete = async () => {
|
||||
const ids = [...selectedKeys];
|
||||
const results = await Promise.allSettled(ids.map((id) => deleteProduct(String(id), true)));
|
||||
const ok = results.filter((r) => r.status === 'fulfilled').length;
|
||||
const fail = results.length - ok;
|
||||
if (fail > 0) message.warning(`已删除 ${ok} 个商品,${fail} 个失败`);
|
||||
else message.success(`已删除 ${ok} 个商品`);
|
||||
setSelectedKeys([]);
|
||||
// 当前页删空时回退一页,避免停在空页
|
||||
if (ok > 0 && ok === items.length && page > 1) setPage(page - 1);
|
||||
else load();
|
||||
};
|
||||
|
||||
const columns: ColumnsType<ProductListItem> = [
|
||||
{
|
||||
title: '商品名',
|
||||
@@ -140,38 +161,67 @@ export default function CollectionPage() {
|
||||
<Title level={4} style={{ marginTop: 0 }}>
|
||||
采集箱
|
||||
</Title>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Input.Search
|
||||
placeholder="搜索名称 / 货号"
|
||||
allowClear
|
||||
style={{ width: 260 }}
|
||||
onSearch={(v) => {
|
||||
setPage(1);
|
||||
setQ(v);
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
value={stage ?? ''}
|
||||
onChange={(e) => {
|
||||
setPage(1);
|
||||
setStage(e.target.value || undefined);
|
||||
}}
|
||||
style={{ padding: '4px 8px', borderRadius: 6, border: '1px solid #d9d9d9' }}
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
{Object.entries(STAGE_LABEL).map(([k, v]) => (
|
||||
<option key={k} value={k}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button onClick={load}>刷新</Button>
|
||||
</Space>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Space wrap>
|
||||
<Input.Search
|
||||
placeholder="搜索名称 / 货号"
|
||||
allowClear
|
||||
style={{ width: 260 }}
|
||||
onSearch={(v) => {
|
||||
setPage(1);
|
||||
setQ(v);
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
value={stage ?? ''}
|
||||
onChange={(e) => {
|
||||
setPage(1);
|
||||
setStage(e.target.value || undefined);
|
||||
}}
|
||||
style={{ padding: '4px 8px', borderRadius: 6, border: '1px solid #d9d9d9' }}
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
{Object.entries(STAGE_LABEL).map(([k, v]) => (
|
||||
<option key={k} value={k}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button onClick={load}>刷新</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button onClick={toggleSelectAll} disabled={items.length === 0}>
|
||||
{allSelected ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`确定删除选中的 ${selectedKeys.length} 个商品?`}
|
||||
disabled={selectedKeys.length === 0}
|
||||
onConfirm={onBatchDelete}
|
||||
>
|
||||
<Button danger disabled={selectedKeys.length === 0}>
|
||||
删除{selectedKeys.length > 0 ? `(${selectedKeys.length})` : ''}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedKeys,
|
||||
onChange: (keys) => setSelectedKeys(keys),
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Col, Input, message, Row, Typography } from 'antd';
|
||||
import { Col, Input, message, Row, Typography } from 'antd';
|
||||
import { CopyOutlined, ExportOutlined } from '@ant-design/icons';
|
||||
import { ProductDetail } from '@/services/product';
|
||||
import { copyText } from '@/utils/file';
|
||||
@@ -137,15 +137,9 @@ export default function TrialInfoPanel({ product, onSave }: Props) {
|
||||
addonAfter={
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
{openUrl && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<ExportOutlined />}
|
||||
onClick={() => window.open(openUrl, '_blank', 'noopener')}
|
||||
>
|
||||
打开地址
|
||||
</Button>
|
||||
<a title="打开采买地址" onClick={() => window.open(openUrl, '_blank', 'noopener')}>
|
||||
<ExportOutlined />
|
||||
</a>
|
||||
)}
|
||||
<a title="复制采买地址" onClick={copyPurchaseUrl}>
|
||||
<CopyOutlined />
|
||||
|
||||
@@ -31,21 +31,20 @@ export default function TrialPage() {
|
||||
const { id } = useParams();
|
||||
const [product, setProduct] = useState<ProductDetail | null>(null);
|
||||
const [assets, setAssets] = useState<ProductAsset[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [active, setActive] = useState(SECTIONS[0].id);
|
||||
|
||||
// 静默刷新:不置 loading。置 loading 会让整页退回全屏 Spin、卸载 TrialSuitePanel,
|
||||
// 杀掉生图轮询并丢失勾选/方案状态(表现为「生成中看不到图、上传后页面刷新」)。
|
||||
// 首屏的加载态由 product 为空时的 Spin 兜底。
|
||||
const load = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [p, a] = await Promise.all([getProduct(id), listAssets(id)]);
|
||||
setProduct(p);
|
||||
setAssets(a);
|
||||
} catch (e) {
|
||||
message.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
@@ -83,7 +82,7 @@ export default function TrialPage() {
|
||||
[id, product],
|
||||
);
|
||||
|
||||
if (loading || !product) {
|
||||
if (!product) {
|
||||
return (
|
||||
<div style={{ padding: 80, textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import {
|
||||
Alert, Button, Card, Checkbox, Col, Empty, Image, Input, InputNumber, message, Modal, Popconfirm, Popover, Progress,
|
||||
Alert, App as AntdApp, Button, Card, Checkbox, Col, Empty, Image, Input, InputNumber, Popconfirm, Popover, Progress,
|
||||
Radio, Row, Segmented, Select, Space, Tag, Typography, Upload,
|
||||
} from 'antd';
|
||||
import { DownloadOutlined, SettingOutlined, ThunderboltOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
@@ -145,10 +145,10 @@ function loadWatermark(): WatermarkPayload {
|
||||
}
|
||||
|
||||
export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Props) {
|
||||
// 素材选择(默认全选主图 + SKU 图,SKU 规格名供 AI 规划 variant 绑定)
|
||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(
|
||||
() => new Set(assets.filter((a) => ['main', 'sku'].includes(a.group_key) && a.type !== 'video').map((a) => a.id)),
|
||||
);
|
||||
// message / modal 用 App 上下文版本:静态方法吃不到 ConfigProvider 动态主题(控制台告警)
|
||||
const { modal, message } = AntdApp.useApp();
|
||||
// 素材选择(默认全部不勾选,用户按需勾选参考底图;SKU 规格名供 AI 规划 variant 绑定,与勾选无关)
|
||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(() => new Set());
|
||||
/** 上传成功待自动勾选的 asset id */
|
||||
const pendingSelectRef = useRef<Set<string>>(new Set());
|
||||
const [uploadingCount, setUploadingCount] = useState(0);
|
||||
@@ -169,6 +169,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
const [suite, setSuite] = useState<SuiteInfo | null>(null);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [exportingZip, setExportingZip] = useState(false);
|
||||
const [exportingResult, setExportingResult] = useState(false);
|
||||
const [exportingImages, setExportingImages] = useState(false);
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
/** 规划请求序号:重置后丢弃迟到的过期响应 */
|
||||
@@ -182,6 +183,16 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
const generatedAssets = useMemo(() => assets.filter((a) => a.group_key === 'generated'), [assets]);
|
||||
const groupImages = (g: string) => imgs.filter((a) => a.group_key === g);
|
||||
const assetUrl = (a: ProductAsset) => a.stored_url || a.source_url;
|
||||
// 图名:取 URL 路径末段(采集图为源站文件名;上传图为落盘文件名),无路径时回退规格名
|
||||
const assetName = (a: ProductAsset): string => {
|
||||
const raw = a.source_url || a.stored_url || '';
|
||||
try {
|
||||
const seg = decodeURIComponent(new URL(raw, window.location.origin).pathname.split('/').pop() || '');
|
||||
return seg || a.variant_name || '未命名';
|
||||
} catch {
|
||||
return a.variant_name || '未命名';
|
||||
}
|
||||
};
|
||||
const rawObj = (product.raw ?? {}) as Record<string, unknown>;
|
||||
const productTitle = ((rawObj.title_zh as string) || (rawObj.title as string) || product.name || '').trim();
|
||||
|
||||
@@ -402,7 +413,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
message.warning('出图方案的张数都是 0');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '生成电商套图',
|
||||
content: (
|
||||
<div>
|
||||
@@ -432,6 +443,38 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
}
|
||||
};
|
||||
|
||||
// ── 导出全部生图结果(套图成功图 + 单张 AI 生图回写图,分组建文件夹) ──
|
||||
const handleExportResult = async () => {
|
||||
const suiteOk = (suite?.images ?? []).filter((i) => i.status === 'ok');
|
||||
if (suiteOk.length === 0 && generatedAssets.length === 0) {
|
||||
message.warning('暂无生图结果可导出');
|
||||
return;
|
||||
}
|
||||
setExportingResult(true);
|
||||
try {
|
||||
const images = [
|
||||
...suiteOk.map((img, i) => ({
|
||||
url: img.url,
|
||||
groupName: '套图结果',
|
||||
variantName: null,
|
||||
key: `suite-${String(i + 1).padStart(3, '0')}`,
|
||||
})),
|
||||
...generatedAssets.map((a, i) => ({
|
||||
url: assetUrl(a),
|
||||
groupName: '单张生图',
|
||||
variantName: a.variant_name,
|
||||
key: `single-${String(i + 1).padStart(3, '0')}`,
|
||||
})),
|
||||
];
|
||||
const blob = await exportImages({ title: `${cleanFilename(productTitle) || '生图'}-生图结果`, images });
|
||||
downloadBlob(blob, `${cleanFilename(productTitle) || '生图'}-生图结果.zip`);
|
||||
} catch (e) {
|
||||
message.error(apiErrorMessage(e));
|
||||
} finally {
|
||||
setExportingResult(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setPlanCount = (idx: number, count: number) => {
|
||||
setPlan((prev) => prev.map((p, i) => (i === idx ? { ...p, count } : p)));
|
||||
};
|
||||
@@ -571,76 +614,89 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(8, minmax(0, 1fr))', gap: 10 }}>
|
||||
{groupImages(g).map((a) => {
|
||||
const on = selectedKeys.has(a.id);
|
||||
return (
|
||||
<div key={a.id} style={{ width: 112, position: 'relative' }}>
|
||||
<div key={a.id}>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
border: `2px solid ${on ? '#1677ff' : 'transparent'}`,
|
||||
borderRadius: 8,
|
||||
padding: 2,
|
||||
background: on ? 'rgba(22,119,255,0.06)' : undefined,
|
||||
width: 'fit-content',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={assetUrl(a)}
|
||||
width={100}
|
||||
height={100}
|
||||
style={{ objectFit: 'cover', borderRadius: 6 }}
|
||||
width="100%"
|
||||
style={{ objectFit: 'cover', borderRadius: 6, aspectRatio: '1 / 1', display: 'block' }}
|
||||
wrapperStyle={{ width: '100%', display: 'block' }}
|
||||
fallback="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><rect width='100' height='100' fill='%23eee'/><text x='22' y='52' font-size='11' fill='%23999'>无预览</text></svg>"
|
||||
/>
|
||||
<span
|
||||
title="勾选为参考底图"
|
||||
onClick={() => toggleKey(a.id)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: on ? '#1677ff' : 'rgba(255,255,255,0.9)',
|
||||
color: on ? '#fff' : '#bbb',
|
||||
border: '1px solid #d9d9d9',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
zIndex: 2,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
{a.status !== 'uploaded' && (
|
||||
<Tag
|
||||
color={a.status === 'failed' ? 'red' : 'default'}
|
||||
style={{ position: 'absolute', bottom: 6, left: 6, margin: 0, zIndex: 2 }}
|
||||
>
|
||||
{a.status === 'failed' ? '转存失败' : '转存中'}
|
||||
</Tag>
|
||||
)}
|
||||
{/* AI 生图:半透明悬浮在图片右下角 */}
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setGenModal({ url: assetUrl(a), name: a.variant_name || a.id })}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 6,
|
||||
bottom: 6,
|
||||
zIndex: 2,
|
||||
background: 'rgba(0, 0, 0, 0.45)',
|
||||
color: '#fff',
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
AI 生图
|
||||
</Button>
|
||||
</div>
|
||||
<span
|
||||
title="勾选为参考底图"
|
||||
onClick={() => toggleKey(a.id)}
|
||||
<div
|
||||
title={a.variant_name || assetName(a)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
left: 6,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
background: on ? '#1677ff' : 'rgba(255,255,255,0.9)',
|
||||
color: on ? '#fff' : '#bbb',
|
||||
border: '1px solid #d9d9d9',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
zIndex: 1,
|
||||
userSelect: 'none',
|
||||
fontSize: 11,
|
||||
color: '#999',
|
||||
marginTop: 2,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
{a.variant_name && (
|
||||
<Tag
|
||||
style={{ position: 'absolute', top: 6, right: 6, margin: 0, zIndex: 1 }}
|
||||
>
|
||||
{a.variant_name}
|
||||
</Tag>
|
||||
)}
|
||||
{a.status !== 'uploaded' && (
|
||||
<Tag
|
||||
color={a.status === 'failed' ? 'red' : 'default'}
|
||||
style={{ position: 'absolute', bottom: 40, left: 6, margin: 0, zIndex: 1 }}
|
||||
>
|
||||
{a.status === 'failed' ? '转存失败' : '转存中'}
|
||||
</Tag>
|
||||
)}
|
||||
<Button
|
||||
block
|
||||
size="small"
|
||||
style={{ marginTop: 4 }}
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => setGenModal({ url: assetUrl(a), name: a.variant_name || a.id })}
|
||||
>
|
||||
AI 生图
|
||||
</Button>
|
||||
{a.variant_name || assetName(a)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -854,6 +910,16 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
水印
|
||||
</Button>
|
||||
</Popover>
|
||||
{/* 导出全部生图结果(套图 + 单张生图) */}
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
loading={exportingResult}
|
||||
disabled={(!suite || !['done', 'partial'].includes(suite.status)) && generatedAssets.length === 0}
|
||||
onClick={handleExportResult}
|
||||
>
|
||||
导出图片
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
@@ -950,9 +1016,9 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
|
||||
{generatedAssets.length > 0 && (
|
||||
<div style={suite ? { borderTop: '1px dashed #e5e7eb', paddingTop: 12, marginTop: 14 } : {}}>
|
||||
<Text strong>
|
||||
单张 AI 生图
|
||||
生图记录
|
||||
<Text type="secondary" style={{ fontWeight: 400, fontSize: 11, marginLeft: 8 }}>
|
||||
(采集图片区「AI 生图」的结果 · {generatedAssets.length} 张)
|
||||
(一键生成与单张生图的落库历史,刷新后仍保留 · {generatedAssets.length} 张)
|
||||
</Text>
|
||||
</Text>
|
||||
<Image.PreviewGroup>
|
||||
|
||||
@@ -5,9 +5,8 @@ import { envConfig } from '@/config/env';
|
||||
const apiClient: AxiosInstance = axios.create({
|
||||
baseURL: envConfig.apiBaseUrl,
|
||||
timeout: 120000, // 图生图较慢,放宽到 120s
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
// 不设全局 Content-Type:axios 1.x 遇到「默认 JSON 头 + FormData」会把表单转成 JSON 发送,
|
||||
// 导致 /api/materials/bytes 等表单接口 422(全部字段 missing)。对象请求体 axios 会自动补 JSON 头。
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
|
||||
@@ -54,21 +54,22 @@ export const PLATFORM_SPECS: Record<string, { lang: string; ratio: string; label
|
||||
ozon: { lang: '俄文', ratio: '3:4', label: 'Ozon' },
|
||||
};
|
||||
|
||||
/** 生图模型(服务端按模型名路由 provider,同 image-suite-studio) */
|
||||
/** 生图模型(服务端按模型名路由 provider;同名模型多家中转时 value 用「模型@供应商」区分,label 带供应商名) */
|
||||
export const IMAGE_MODEL_OPTIONS = [
|
||||
{ value: 'qwen-image-3.0-pro', label: 'qwen-image-3.0-pro', desc: '同步生成,响应快、图文理解强,适合快速批量出图' },
|
||||
{ value: 'wan2.7-image-pro', label: 'wan2.7-image-pro', desc: '异步精修,质感与细节更强,适合高质量电商大片' },
|
||||
{ value: 'wan2.6-image', label: 'wan2.6-image', desc: '通义 2.6 图生图,支持参考图与多图融合,速度更快、稳定性好' },
|
||||
{ value: 'wan2.6-t2i', label: 'wan2.6-t2i', desc: '通义 2.6 纯文生图,不使用参考图(商品外观靠文案描述),速度最快' },
|
||||
{ value: 'gpt-image-2', label: 'gpt-image-2', desc: 'GPT 图像模型,构图与图内文案渲染最强,参考图高保真,单张 1-5 分钟' },
|
||||
{ value: 'gpt-image-2-vip', label: 'gpt-image-2-vip', desc: 'GPT 官逆低价通道,构图与文字渲染强、成本更低,适合大批量出图' },
|
||||
{ value: 'nano-banana', label: 'nano-banana', desc: 'Google Gemini 图像模型,出图极快,图像编辑与风格迁移强,多图融合自然' },
|
||||
{ value: 'nano-banana-2', label: 'nano-banana-2', desc: 'Google 新一代图像模型,画质与文字渲染大幅提升,日常生成与改图的综合首选' },
|
||||
{ value: 'nano-banana-2-lite', label: 'nano-banana-2-lite', desc: 'nano-banana-2 轻量版,约 4 秒/张、成本极低,适合大批量出图与快速试错' },
|
||||
{ value: 'nano-banana-pro', label: 'nano-banana-pro', desc: 'Google 最高保真旗舰,细节最强、支持 4K 输出,适合商业级精修大片' },
|
||||
{ value: 'qwen-image-3.0-pro', label: 'qwen-image-3.0-pro(阿里巴巴)', desc: '同步生成,响应快、图文理解强,适合快速批量出图' },
|
||||
{ value: 'wan2.7-image-pro', label: 'wan2.7-image-pro(阿里巴巴)', desc: '异步精修,质感与细节更强,适合高质量电商大片' },
|
||||
{ value: 'wan2.6-image', label: 'wan2.6-image(阿里巴巴)', desc: '通义 2.6 图生图,支持参考图与多图融合,速度更快、稳定性好' },
|
||||
{ value: 'wan2.6-t2i', label: 'wan2.6-t2i(阿里巴巴)', desc: '通义 2.6 纯文生图,不使用参考图(商品外观靠文案描述),速度最快' },
|
||||
{ value: 'gpt-image-2', label: 'gpt-image-2(RightCode)', desc: 'GPT 图像模型,构图与图内文案渲染最强,参考图高保真,单张 1-5 分钟' },
|
||||
{ value: 'gpt-image-2-vip', label: 'gpt-image-2-vip(RightCode)', desc: 'GPT 官逆低价通道,构图与文字渲染强、成本更低,适合大批量出图' },
|
||||
{ value: 'nano-banana', label: 'nano-banana(RightCode)', desc: 'Google Gemini 图像模型,出图极快,图像编辑与风格迁移强,多图融合自然' },
|
||||
{ value: 'nano-banana-2', label: 'nano-banana-2(RightCode)', desc: 'Google 新一代图像模型,画质与文字渲染大幅提升,日常生成与改图的综合首选' },
|
||||
{ value: 'nano-banana-2-lite', label: 'nano-banana-2-lite(RightCode)', desc: 'nano-banana-2 轻量版,约 4 秒/张、成本极低,适合大批量出图与快速试错' },
|
||||
{ value: 'nano-banana-pro', label: 'nano-banana-pro(RightCode)', desc: 'Google 最高保真旗舰,细节最强、支持 4K 输出,适合商业级精修大片' },
|
||||
{ value: 'gpt-image-2@pcore', label: 'gpt-image-2(Pcore)', desc: 'Pcore 中转的 GPT 图像模型,异步任务出图,构图与图内文案渲染强' },
|
||||
] as const;
|
||||
|
||||
export const DEFAULT_IMAGE_MODEL = 'gpt-image-2-vip';
|
||||
export const DEFAULT_IMAGE_MODEL = 'gpt-image-2@pcore';
|
||||
|
||||
export interface WatermarkPayload {
|
||||
enabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user