feat: 图片创作每张图加「加入模特库」按钮 (row41)
图片工作台单图「更多」气泡新增「加入模特库」(与「加入资产库」并列): 把该生成图复用为模特形象图建 Model 条目,加入后变「已加入模特库」禁用。 后端新增 POST /api/models/enroll-asset/(ModelLibraryViewSet.enroll_asset): 复用已有资产作 portrait_asset 建 Model,不重新上传/不二次扣费;同一资产幂等返回。 需部署生效(后端)。 构建: tsc --noEmit=0, npm run build=0; 后端 py_compile OK。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -399,6 +399,29 @@ class ModelLibraryViewSet(ModelViewSet):
|
||||
)
|
||||
return Response(ModelLibrarySerializer(model).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="enroll-asset")
|
||||
def enroll_asset(self, request):
|
||||
"""图片创作「加入模特库」:把一张已生成的图复用为模特形象图建 Model 条目。
|
||||
资产生成时已落库扣过费,此处只引用、不重新上传/不二次扣费。同一资产已建过则幂等返回。"""
|
||||
team = self.get_team()
|
||||
asset_id = request.data.get("asset_id")
|
||||
asset = Asset.objects.filter(id=asset_id, team=team, is_deleted=False).first() if asset_id else None
|
||||
if asset is None:
|
||||
raise ValidationError({"asset_id": "资产不存在或不属于本团队"})
|
||||
existing = Model.objects.filter(team=team, portrait_asset=asset, is_deleted=False).first()
|
||||
if existing is not None:
|
||||
return Response(ModelLibrarySerializer(existing).data, status=status.HTTP_200_OK)
|
||||
name = (request.data.get("name") or (asset.name or "模特").split("·")[0].strip() or "模特")[:255]
|
||||
model = Model.objects.create(
|
||||
team=team,
|
||||
created_by=request.user,
|
||||
name=name,
|
||||
source=Model.Source.AI,
|
||||
portrait_asset=asset,
|
||||
metadata={"source": "enroll_from_asset", "asset_id": str(asset.id)},
|
||||
)
|
||||
return Response(ModelLibrarySerializer(model).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class AssetUploadView(APIView):
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
@@ -574,6 +574,10 @@ export const api = {
|
||||
uploadModel(formData: FormData) {
|
||||
return request<ModelEntity>("/api/models/upload/", { method: "POST", body: formData });
|
||||
},
|
||||
// 图片创作「加入模特库」:把一张已生成的图复用为模特形象图建 Model 条目(不重传、不二次扣费;幂等)
|
||||
enrollModelFromAsset(assetId: string, name?: string) {
|
||||
return request<ModelEntity>("/api/models/enroll-asset/", { method: "POST", body: JSON.stringify({ asset_id: assetId, name }) });
|
||||
},
|
||||
// 删除模特(软删;官方模特不可删)
|
||||
deleteModel(id: string) {
|
||||
return request<void>(`/api/models/${id}/`, { method: "DELETE" });
|
||||
|
||||
@@ -1249,6 +1249,20 @@ export function ImageWorkbenchPage({
|
||||
applyLibraryState(batchId, [assetId], next);
|
||||
}
|
||||
|
||||
/* 图片创作「加入模特库」:把该图复用为模特形象图建 Model 条目(后端不二次扣费;幂等)。
|
||||
成功后该图气泡项变「已加入模特库」禁用,与「加入资产库」同款单图粒度操作。 */
|
||||
const [enrolledModelIds, setEnrolledModelIds] = useState<Set<string>>(new Set());
|
||||
const [enrollingModelId, setEnrollingModelId] = useState("");
|
||||
async function enrollModelFromImage(assetId: string) {
|
||||
if (!assetId || enrollingModelId) return;
|
||||
setEnrollingModelId(assetId);
|
||||
try {
|
||||
await api.enrollModelFromAsset(assetId);
|
||||
setEnrolledModelIds((prev) => new Set(prev).add(assetId));
|
||||
} catch { /* 失败静默,可重试 */ }
|
||||
finally { setEnrollingModelId(""); }
|
||||
}
|
||||
|
||||
/* 单图「再生成」/「重跑这张」(§4.18 悬浮① / YYX#5):
|
||||
PMC#25——补一张图进原批次卡(不新开任务、不覆盖已有好图)。 */
|
||||
async function regenSingleImage(src: GenBatch) {
|
||||
@@ -1403,6 +1417,10 @@ export function ImageWorkbenchPage({
|
||||
<Bookmark size={13} />
|
||||
{inLib ? "取消加入资产库" : "加入资产库"}
|
||||
</button>
|
||||
<button type="button" className="gen-bubble-item" disabled={enrolledModelIds.has(assetId) || enrollingModelId === assetId} onClick={() => { setOpenMore(""); enrollModelFromImage(assetId); }}>
|
||||
<Users size={13} />
|
||||
{enrolledModelIds.has(assetId) ? "已加入模特库" : (enrollingModelId === assetId ? "加入中…" : "加入模特库")}
|
||||
</button>
|
||||
<button type="button" className="gen-bubble-item danger" onClick={() => { setOpenMore(""); removeImageFromBatch(batch.id, assetId); }}>
|
||||
<Trash2 size={13} />
|
||||
删除
|
||||
|
||||
Reference in New Issue
Block a user