| 1 | /** |
| 2 | * VideoCreateDraftTaskWidget - 视频生成草稿任务悬浮窗 |
| 3 | * 在长时间执行期间提供可折叠的进度提示,并提醒用户不要关闭当前页面 |
| 4 | */ |
| 5 | |
| 6 | 'use client' |
| 7 | |
| 8 | import type { PlatType } from '@/app/config/platConfig' |
| 9 | import { ChevronUp, Loader2, Minimize2, Sparkles } from 'lucide-react' |
| 10 | import { memo, useEffect, useMemo } from 'react' |
| 11 | import { useShallow } from 'zustand/react/shallow' |
| 12 | import { useTransClient } from '@/app/i18n/client' |
| 13 | import { Button } from '@/components/ui/button' |
| 14 | |
| 15 | import { usePlanDetailStore } from '@/store/draft-box/planDetailStore' |
| 16 | import { getPlatformInfoSync } from '@/store/platformMetadata' |
| 17 | import { cn } from '@/utils/className' |
| 18 | import { useMediaTabStore } from '../ContentTabs/mediaTabStore' |
| 19 | |
| 20 | export const VideoCreateDraftTaskWidget = memo(() => { |
| 21 | const { t } = useTransClient('material') |
| 22 | |
| 23 | const { |
| 24 | draftCreationTasks, |
| 25 | draftCreationWidgetMinimized, |
| 26 | mediaBatchMode, |
| 27 | setDraftCreationWidgetMinimized, |
| 28 | } = useMediaTabStore( |
| 29 | useShallow(state => ({ |
| 30 | draftCreationTasks: state.draftCreationTasks, |
| 31 | draftCreationWidgetMinimized: state.draftCreationWidgetMinimized, |
| 32 | mediaBatchMode: state.batchMode, |
| 33 | setDraftCreationWidgetMinimized: state.setDraftCreationWidgetMinimized, |
| 34 | })), |
| 35 | ) |
| 36 | |
| 37 | const draftBatchMode = usePlanDetailStore(state => state.batchMode) |
| 38 | |
| 39 | const activeTasks = useMemo(() => { |
| 40 | return [...Object.values(draftCreationTasks)].sort((a, b) => b.startedAt - a.startedAt) |
| 41 | }, [draftCreationTasks]) |
| 42 | |
| 43 | const visibleTasks = activeTasks.slice(0, 2) |
| 44 | const extraTaskCount = Math.max(activeTasks.length - visibleTasks.length, 0) |
| 45 | const hasBottomBar = draftBatchMode || mediaBatchMode |
| 46 | |
| 47 | useEffect(() => { |
| 48 | if (activeTasks.length === 0) |
| 49 | return |
| 50 | |
| 51 | const handler = (e: BeforeUnloadEvent) => { |
| 52 | e.preventDefault() |
| 53 | return '' |
| 54 | } |
| 55 | |
| 56 | window.addEventListener('beforeunload', handler) |
| 57 | return () => window.removeEventListener('beforeunload', handler) |
| 58 | }, [activeTasks.length]) |
| 59 | |
| 60 | if (activeTasks.length === 0) { |
| 61 | return null |
| 62 | } |
| 63 | |
| 64 | const positionClassName = hasBottomBar |
| 65 | ? 'bottom-24 sm:bottom-20' |
| 66 | : 'bottom-4 sm:bottom-6' |
| 67 | |
| 68 | if (draftCreationWidgetMinimized) { |
| 69 | return ( |
| 70 | <button |
| 71 | type="button" |
| 72 | onClick={() => setDraftCreationWidgetMinimized(false)} |
| 73 | className={cn( |
| 74 | 'fixed right-4 z-40 flex w-[min(320px,calc(100vw-32px))] cursor-pointer items-center gap-3 rounded-full border border-primary/20 bg-background/95 px-4 py-3 text-left shadow-2xl shadow-primary/10 backdrop-blur-md transition-all duration-200 hover:border-primary/30 hover:shadow-primary/15', |
| 75 | positionClassName, |
| 76 | )} |
| 77 | > |
| 78 | <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary"> |
| 79 | <Loader2 className="h-4 w-4 animate-spin" /> |
| 80 | </div> |
| 81 | <div className="min-w-0 flex-1"> |
| 82 | <p className="truncate text-sm font-semibold text-foreground"> |
| 83 | {t('mediaManagement.createDraftTaskCompactTitle', { count: activeTasks.length })} |
| 84 | </p> |
| 85 | <p className="truncate text-xs text-muted-foreground"> |
| 86 | {t('mediaManagement.createDraftTaskCompactDesc')} |
| 87 | </p> |
| 88 | </div> |
| 89 | <ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" /> |
| 90 | </button> |
| 91 | ) |
| 92 | } |
| 93 | |
| 94 | return ( |
| 95 | <div |
| 96 | className={cn( |
| 97 | 'fixed right-4 z-40 w-[min(380px,calc(100vw-32px))] rounded-2xl border border-border bg-background/95 p-4 shadow-2xl backdrop-blur-md', |
| 98 | positionClassName, |
| 99 | )} |
| 100 | > |
| 101 | <div className="flex items-start gap-3"> |
| 102 | <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-primary"> |
| 103 | <Sparkles className="h-4 w-4" /> |
| 104 | </div> |
| 105 | |
| 106 | <div className="min-w-0 flex-1"> |
| 107 | <div className="flex items-start justify-between gap-3"> |
| 108 | <div className="min-w-0"> |
| 109 | <p className="text-sm font-semibold text-foreground"> |
| 110 | {t('mediaManagement.createDraftTaskRunningTitle')} |
| 111 | </p> |
| 112 | <p className="mt-1 text-xs leading-5 text-muted-foreground"> |
| 113 | {t('mediaManagement.createDraftTaskRunningDesc')} |
| 114 | </p> |
| 115 | </div> |
| 116 | |
| 117 | <Button |
| 118 | type="button" |
| 119 | variant="ghost" |
| 120 | size="icon" |
| 121 | onClick={() => setDraftCreationWidgetMinimized(true)} |
| 122 | className="h-8 w-8 shrink-0 rounded-full" |
| 123 | > |
| 124 | <Minimize2 className="h-4 w-4" /> |
| 125 | <span className="sr-only">{t('mediaManagement.createDraftTaskCollapse')}</span> |
| 126 | </Button> |
| 127 | </div> |
| 128 | |
| 129 | <div className="mt-3 inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/10 px-2.5 py-1 text-[11px] font-medium text-primary"> |
| 130 | <Loader2 className="h-3.5 w-3.5 animate-spin" /> |
| 131 | {t('mediaManagement.createDraftTaskCount', { count: activeTasks.length })} |
| 132 | </div> |
| 133 | </div> |
| 134 | </div> |
| 135 | |
| 136 | <div className="mt-4 space-y-2.5"> |
| 137 | {visibleTasks.map((task, index) => { |
| 138 | const platformNames = task.platforms |
| 139 | .map((platform) => { |
| 140 | return getPlatformInfoSync(platform as PlatType)?.name || platform |
| 141 | }) |
| 142 | .join(' / ') |
| 143 | |
| 144 | return ( |
| 145 | <div |
| 146 | key={task.mediaId} |
| 147 | className="rounded-xl border border-border/80 bg-muted/35 px-3 py-2.5" |
| 148 | > |
| 149 | <p className="line-clamp-1 text-sm font-medium text-foreground"> |
| 150 | {task.mediaTitle || t('mediaManagement.createDraftTaskUntitled', { index: index + 1 })} |
| 151 | </p> |
| 152 | <p className="mt-1 text-xs text-muted-foreground"> |
| 153 | {t('mediaManagement.createDraftTaskEta')} |
| 154 | </p> |
| 155 | <p className="mt-1 line-clamp-1 text-xs text-muted-foreground"> |
| 156 | {t('mediaManagement.createDraftTaskPlatforms', { |
| 157 | platforms: platformNames, |
| 158 | interpolation: { escapeValue: false }, |
| 159 | })} |
| 160 | </p> |
| 161 | </div> |
| 162 | ) |
| 163 | })} |
| 164 | |
| 165 | {extraTaskCount > 0 && ( |
| 166 | <p className="px-1 text-xs text-muted-foreground"> |
| 167 | {t('mediaManagement.createDraftTaskMore', { count: extraTaskCount })} |
| 168 | </p> |
| 169 | )} |
| 170 | </div> |
| 171 | |
| 172 | <div className="mt-4 rounded-xl border border-border bg-muted/45 px-3 py-2.5"> |
| 173 | <p className="text-xs leading-5 text-muted-foreground"> |
| 174 | {t('mediaManagement.createDraftTaskDoNotClose')} |
| 175 | </p> |
| 176 | </div> |
| 177 | </div> |
| 178 | ) |
| 179 | }) |
| 180 | |
| 181 | VideoCreateDraftTaskWidget.displayName = 'VideoCreateDraftTaskWidget' |
| 182 |