diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index baa1a22..57bcfc1 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -320,7 +320,10 @@ def _release_freeze(record): frozen = record.frozen_amount with transaction.atomic(): locked_team = Team.objects.select_for_update().get(pk=team.pk) - locked_team.frozen_amount = F('frozen_amount') - frozen + # 防止 frozen_amount 变负 + actual_release = min(frozen, locked_team.frozen_amount) + if actual_release > 0: + locked_team.frozen_amount = F('frozen_amount') - actual_release locked_team.total_seconds_used = F('total_seconds_used') - record.seconds_consumed locked_team.save(update_fields=['frozen_amount', 'total_seconds_used']) record.frozen_amount = 0 diff --git a/web/src/components/VideoDetailModal.tsx b/web/src/components/VideoDetailModal.tsx index 48c5dfc..067d8f2 100644 --- a/web/src/components/VideoDetailModal.tsx +++ b/web/src/components/VideoDetailModal.tsx @@ -13,13 +13,14 @@ interface Props { onReEdit?: (id: string) => void; onRegenerate?: (id: string) => void; onDelete?: (id: string) => void; + hideReEdit?: boolean; onPrev?: () => void; onNext?: () => void; hasPrev?: boolean; hasNext?: boolean; } -export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDelete, onPrev, onNext, hasPrev, hasNext }: Props) { +export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDelete, onPrev, onNext, hasPrev, hasNext, hideReEdit }: Props) { const navigate = useNavigate(); const videoRef = useRef(null); const videoContainerRef = useRef(null); @@ -230,7 +231,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele } } onClose(); - navigate('/'); + navigate('/app'); } }; @@ -497,7 +498,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele {/* Re-edit button above info bar */} -
+ {!hideReEdit &&
-
+
} {/* Fixed bottom: info bar + actions card */}
diff --git a/web/src/pages/AdminAssetsPage.tsx b/web/src/pages/AdminAssetsPage.tsx index a6197ad..c17c511 100644 --- a/web/src/pages/AdminAssetsPage.tsx +++ b/web/src/pages/AdminAssetsPage.tsx @@ -229,6 +229,7 @@ export function AdminAssetsPage() { setDetailTask(null)} + hideReEdit />
); diff --git a/web/src/store/generation.ts b/web/src/store/generation.ts index 5918af4..6cf27e6 100644 --- a/web/src/store/generation.ts +++ b/web/src/store/generation.ts @@ -75,7 +75,7 @@ function backendToFrontend(bt: BackendTask): GenerationTask { duration: bt.duration as GenerationTask['duration'], references, status: mapStatus(bt.status), - progress: mapProgress(bt.status), + progress: bt.status === 'processing' ? Number(sessionStorage.getItem(`progress_${bt.task_id}`) || mapProgress(bt.status)) : mapProgress(bt.status), resultUrl: bt.result_url || undefined, errorMessage: mapErrorMessage(bt.error_message), createdAt: new Date(bt.created_at).getTime(), @@ -105,7 +105,9 @@ function ensureSmoothProgress() { if (t.status !== 'generating') return t; // Decelerate: fast at start, slow near end const increment = t.progress < 30 ? 2 : t.progress < 60 ? 1 : 0.5; - return { ...t, progress: Math.min(t.progress + increment, 95) }; + const newProgress = Math.min(t.progress + increment, 95); + if (t.taskId) sessionStorage.setItem(`progress_${t.taskId}`, String(newProgress)); + return { ...t, progress: newProgress }; }), })); }, 2000); @@ -146,6 +148,7 @@ function startPolling(taskId: string, frontendId: string) { if (newStatus === 'completed' || newStatus === 'failed') { pollTimers.delete(frontendId); + sessionStorage.removeItem(`progress_${taskId}`); if (newStatus === 'completed') { useAuthStore.getState().fetchUserInfo(); }