refactor(server): 移除已放弃的 Ozon API 直传模块,新增素材删除接口

- 删除 shops/publish/categories/ozon 相关路由、模型、schema 与客户端服务(V2.1 决策放弃 API 直传,改人工上传)
- materials 新增 DELETE /assets/{id}:删除素材记录与本地文件,并同步修正 asset_counts
- 试算页支持单张素材删除,生图弹窗交互微调
- 新增 docs/v2.1/HANDOFF.md 工作交接说明,README 补充索引
This commit is contained in:
R524809
2026-08-28 17:47:57 +08:00
parent dc6d38c128
commit a77ec26a02
20 changed files with 248 additions and 1010 deletions
+3 -2
View File
@@ -9,7 +9,7 @@ interface Props {
open: boolean;
productId: string;
/** 待生成的源图(采集图或生成图) */
source: { url: string; name: string } | null;
source: { url: string; name: string; afterAssetId?: string } | null;
onClose: () => void;
/** 生成成功回调(服务端 append 后刷新素材列表) */
onGenerated?: (url: string) => void;
@@ -49,9 +49,10 @@ export default function AiImageGenModal({ open, productId, source, onClose, onGe
prompt: prompt.trim(),
model,
append: true,
after_asset_id: source.afterAssetId,
});
setResultUrl(r.url);
message.success('生成完成,已追加到「生成图」分组');
message.success('生成完成,已追加到「生成结果」');
onGenerated?.(r.url);
} catch (e) {
message.error(apiErrorMessage(e));
+59 -8
View File
@@ -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, Popover, Progress,
Alert, Button, Card, Checkbox, Col, Empty, Image, Input, InputNumber, message, Modal, Popconfirm, Popover, Progress,
Radio, Row, Segmented, Select, Space, Tag, Typography, Upload,
} from 'antd';
import { DownloadOutlined, SettingOutlined, ThunderboltOutlined, UploadOutlined } from '@ant-design/icons';
@@ -14,7 +14,7 @@ import { apiErrorMessage } from '@/services/api';
import {
DEFAULT_IMAGE_MODEL, DEFAULT_PLAN, DEFAULT_WATERMARK, IMAGE_MODEL_OPTIONS,
STYLE_SET_OPTIONS, SuiteInfo, SuiteTextPayload, WatermarkPayload,
downloadSuiteZip, exportImages, generateSuite, getSuite, planSuite, uploadProductAsset,
deleteAsset, downloadSuiteZip, exportImages, generateSuite, getSuite, planSuite, uploadProductAsset,
type PlanItem,
} from '@/services/suite';
import { cleanFilename, downloadBlob } from '@/utils/file';
@@ -174,8 +174,8 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
/** 规划请求序号:重置后丢弃迟到的过期响应 */
const planSeqRef = useRef(0);
// 单张 AI 生图弹窗
const [genModal, setGenModal] = useState<{ url: string; name: string } | null>(null);
// 单张 AI 生图弹窗afterAssetId:结果插到该素材之后,方便对比)
const [genModal, setGenModal] = useState<{ url: string; name: string; afterAssetId?: string } | null>(null);
// 采集图片卡只放采集/上传素材;单张 AI 生图结果(generated)放「生成结果」卡展示
const imgs = useMemo(() => assets.filter((a) => a.type !== 'video' && a.group_key !== 'generated'), [assets]);
@@ -510,6 +510,17 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
);
const selectableCount = imgs.length;
/** 删除一张已生成的图片(二次确认后调用),成功后刷新素材列表 */
const handleDeleteGenerated = async (assetId: string) => {
try {
await deleteAsset(assetId);
message.success('已删除');
onRefreshAssets();
} catch (e) {
message.error(apiErrorMessage(e));
}
};
const displayGroups = DISPLAY_GROUPS.filter((g) => groupImages(g).length > 0);
return (
@@ -947,7 +958,7 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
<Image.PreviewGroup>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginTop: 10 }}>
{generatedAssets.map((a) => (
<div key={a.id} style={{ width: 112 }}>
<div key={a.id} style={{ width: 100, position: 'relative' }}>
<Image
src={assetUrl(a)}
width={100}
@@ -955,18 +966,58 @@ export default function TrialSuitePanel({ product, assets, onRefreshAssets }: Pr
style={{ objectFit: 'cover', borderRadius: 6 }}
fallback="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='133'><rect width='100' height='133' fill='%23eee'/><text x='18' y='70' font-size='11' fill='%23999'>无预览</text></svg>"
/>
{/* 删除(二次确认) */}
<Popconfirm
title="删除图片"
description="确认删除这张已生成的图片?"
okText="删除"
cancelText="取消"
okButtonProps={{ danger: true }}
onConfirm={() => handleDeleteGenerated(a.id)}
>
<span
title="删除这张图片"
onClick={(e) => e.stopPropagation()}
style={{
position: 'absolute',
top: 6,
right: 6,
width: 20,
height: 20,
borderRadius: '50%',
background: 'rgba(255,255,255,0.95)',
color: '#ff4d4f',
border: '1px solid #ffccc7',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontSize: 12,
zIndex: 1,
userSelect: 'none',
lineHeight: 1,
}}
>
</span>
</Popconfirm>
<div
style={{
fontSize: 11,
color: 'rgba(0,0,0,0.45)',
marginTop: 2,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={a.stored_url ?? undefined}
title="基于这张图再次 AI 编辑"
onClick={() => setGenModal({ url: assetUrl(a), name: a.variant_name || 'AI 编辑', afterAssetId: a.id })}
>
AI {a.created_at ? new Date(a.created_at).toLocaleTimeString() : ''}
<a style={{ color: '#1677ff' }}>AI </a>
{a.created_at && (
<span style={{ color: 'rgba(0,0,0,0.45)', marginLeft: 4 }}>
{new Date(a.created_at).toLocaleTimeString()}
</span>
)}
</div>
</div>
))}
+7
View File
@@ -161,6 +161,8 @@ export interface ImageEditSinglePayload {
prompt: string;
model: string;
append?: boolean;
/** 插入锚点:新素材排在该素材之后(方便与原图对比) */
after_asset_id?: string;
}
/** ★ 单张 AI 生图(采集图/生成图上的「AI生图」入口) */
@@ -168,6 +170,11 @@ export function imageEditSingle(payload: ImageEditSinglePayload) {
return api.post<{ url: string; asset_id: string | null }>('/suite/image-edit', payload);
}
/** 删除一条素材(DB 记录 + 本地文件),用于删除已生成的图片 */
export function deleteAsset(assetId: string) {
return api.delete<{ deleted: boolean }>(`/assets/${assetId}`);
}
export interface ExportImagesPayload {
title: string;
images: Array<{ url: string; groupName: string; variantName?: string | null; key: string }>;