返回 AiToEarn
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
182 lines Plain Text