fix: v0.10.2 — admin重新编辑隐藏/frozen防负数/进度条刷新保持/navigate修正

- admin资产页视频详情隐藏「重新编辑」按钮(hideReEdit prop)
- 团管重新编辑跳转修正:navigate('/') → navigate('/app')
- _release_freeze 防止 frozen_amount 变负数
- 生成进度条用 sessionStorage 持久化,刷新页面后从之前位置继续

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-21 00:24:14 +08:00
parent ef2212e345
commit 699a390f45
4 changed files with 15 additions and 7 deletions

View File

@ -320,7 +320,10 @@ def _release_freeze(record):
frozen = record.frozen_amount frozen = record.frozen_amount
with transaction.atomic(): with transaction.atomic():
locked_team = Team.objects.select_for_update().get(pk=team.pk) 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.total_seconds_used = F('total_seconds_used') - record.seconds_consumed
locked_team.save(update_fields=['frozen_amount', 'total_seconds_used']) locked_team.save(update_fields=['frozen_amount', 'total_seconds_used'])
record.frozen_amount = 0 record.frozen_amount = 0

View File

@ -13,13 +13,14 @@ interface Props {
onReEdit?: (id: string) => void; onReEdit?: (id: string) => void;
onRegenerate?: (id: string) => void; onRegenerate?: (id: string) => void;
onDelete?: (id: string) => void; onDelete?: (id: string) => void;
hideReEdit?: boolean;
onPrev?: () => void; onPrev?: () => void;
onNext?: () => void; onNext?: () => void;
hasPrev?: boolean; hasPrev?: boolean;
hasNext?: 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 navigate = useNavigate();
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const videoContainerRef = useRef<HTMLDivElement>(null); const videoContainerRef = useRef<HTMLDivElement>(null);
@ -230,7 +231,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele
} }
} }
onClose(); onClose();
navigate('/'); navigate('/app');
} }
}; };
@ -497,7 +498,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele
</div> </div>
{/* Re-edit button above info bar */} {/* Re-edit button above info bar */}
<div style={{ padding: '0 20px 12px', borderBottom: '1px solid rgba(255,255,255,0.08)' }}> {!hideReEdit && <div style={{ padding: '0 20px 12px', borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<button className={styles.cardBtn} onClick={handleReEdit} style={{ width: '100%', justifyContent: 'center' }}> <button className={styles.cardBtn} onClick={handleReEdit} style={{ width: '100%', justifyContent: 'center' }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
@ -505,7 +506,7 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele
</svg> </svg>
</button> </button>
</div> </div>}
{/* Fixed bottom: info bar + actions card */} {/* Fixed bottom: info bar + actions card */}
<div className={styles.infoPanelBottom}> <div className={styles.infoPanelBottom}>

View File

@ -229,6 +229,7 @@ export function AdminAssetsPage() {
<VideoDetailModal <VideoDetailModal
task={detailTask} task={detailTask}
onClose={() => setDetailTask(null)} onClose={() => setDetailTask(null)}
hideReEdit
/> />
</div> </div>
); );

View File

@ -75,7 +75,7 @@ function backendToFrontend(bt: BackendTask): GenerationTask {
duration: bt.duration as GenerationTask['duration'], duration: bt.duration as GenerationTask['duration'],
references, references,
status: mapStatus(bt.status), 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, resultUrl: bt.result_url || undefined,
errorMessage: mapErrorMessage(bt.error_message), errorMessage: mapErrorMessage(bt.error_message),
createdAt: new Date(bt.created_at).getTime(), createdAt: new Date(bt.created_at).getTime(),
@ -105,7 +105,9 @@ function ensureSmoothProgress() {
if (t.status !== 'generating') return t; if (t.status !== 'generating') return t;
// Decelerate: fast at start, slow near end // Decelerate: fast at start, slow near end
const increment = t.progress < 30 ? 2 : t.progress < 60 ? 1 : 0.5; 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); }, 2000);
@ -146,6 +148,7 @@ function startPolling(taskId: string, frontendId: string) {
if (newStatus === 'completed' || newStatus === 'failed') { if (newStatus === 'completed' || newStatus === 'failed') {
pollTimers.delete(frontendId); pollTimers.delete(frontendId);
sessionStorage.removeItem(`progress_${taskId}`);
if (newStatus === 'completed') { if (newStatus === 'completed') {
useAuthStore.getState().fetchUserInfo(); useAuthStore.getState().fetchUserInfo();
} }