返回 AiToEarn
1 /**
2 * DraftContentModule - 内容管理核心模块
3 * 可复用的草稿管理区域,包含 AI生成栏、草稿列表、相关弹框
4 */
5
6 'use client'
7
8 import type { DraftListSectionTab } from '../DraftListSection'
9 import { Loader2 } from 'lucide-react'
10 import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
11 import { useShallow } from 'zustand/react/shallow'
12 import { PubType } from '@/app/config/publishConfig'
13 import PublishDialog from '@/components/PublishDialog'
14 import { buildPublishParamsFromDraft } from '@/components/PublishDialog/PublishDialog.util'
15 import { usePublishDialog } from '@/components/PublishDialog/usePublishDialog'
16 import { useAccountStore } from '@/store/account'
17 import { usePlanDetailStore } from '@/store/draft-box/planDetailStore'
18 import { getPlatformInfoSync } from '@/store/platformMetadata'
19 import { isPlatformAvailable } from '@/store/platformMetadata/utils'
20 import { useUserStore } from '@/store/user'
21 import { useGenerationPolling } from '../../hooks/useGenerationPolling'
22 import AiBatchGenerateBar from '../AiBatchGenerateBar'
23 import { useMediaTabStore } from '../ContentTabs/mediaTabStore'
24 import { CreateMaterialModal } from '../CreateMaterialModal'
25 import { DraftDetailDialog } from '../DraftDetailDialog'
26 import { DraftListSection } from '../DraftListSection'
27 import { GenerationDetailDialog } from '../GenerationDetailDialog'
28 import { TransferDraftDialog } from '../TransferDraftDialog'
29 import { VideoCreateDraftTaskWidget } from '../VideoCreateDraftTaskWidget'
30
31 interface DraftContentModuleProps {
32 /** 外部指定草稿箱 ID,不依赖当前草稿箱 */
33 groupId?: string
34 /** 草稿列表可见 Tab */
35 draftListTabs?: DraftListSectionTab[]
36 /** 草稿列表默认 Tab */
37 draftListDefaultTab?: DraftListSectionTab
38 /** 是否强制使用草稿生成模式 */
39 forceDraftMode?: boolean
40 /** 是否允许转移草稿 */
41 allowTransfer?: boolean
42 /** 是否显示视频生成草稿长任务悬浮窗 */
43 showVideoCreateDraftTaskWidget?: boolean
44 /** 内容区域外层样式 */
45 contentClassName?: string
46 /** 是否嵌入到其他弹框/面板中 */
47 embedded?: boolean
48 /** 是否允许拖拽草稿/素材到发布弹框 */
49 enablePublishDrag?: boolean
50 }
51
52 function DraftContentModule({
53 groupId,
54 draftListTabs,
55 draftListDefaultTab,
56 forceDraftMode = false,
57 allowTransfer = true,
58 showVideoCreateDraftTaskWidget = true,
59 contentClassName = 'space-y-6 p-4 md:p-6',
60 embedded = false,
61 enablePublishDrag = false,
62 }: DraftContentModuleProps) {
63 const {
64 currentPlan,
65 createMaterialModalOpen,
66 editingMaterial,
67 generationTasks,
68 publishDialogOpen,
69 publishingDraft,
70 closeMaterialModal,
71 fetchMaterials,
72 closePublishDialog,
73 syncGenerationTasks,
74 updateGeneratingCount,
75 } = usePlanDetailStore(
76 useShallow(state => ({
77 currentPlan: state.currentPlan,
78 createMaterialModalOpen: state.createMaterialModalOpen,
79 editingMaterial: state.editingMaterial,
80 generationTasks: state.generationTasks,
81 publishDialogOpen: state.publishDialogOpen,
82 publishingDraft: state.publishingDraft,
83 closeMaterialModal: state.closeMaterialModal,
84 fetchMaterials: state.fetchMaterials,
85 closePublishDialog: state.closePublishDialog,
86 syncGenerationTasks: state.syncGenerationTasks,
87 updateGeneratingCount: state.updateGeneratingCount,
88 })),
89 )
90
91 const selectedPlanId = groupId || currentPlan?.id || null
92 const previousMediaResetPlanIdRef = useRef<string | null>(null)
93 const pollingTaskIds = useMemo(
94 () => generationTasks.filter(task => task.status === 'generating').map(task => task.id),
95 [generationTasks],
96 )
97
98 const accountList = useAccountStore(state => state.accountList)
99
100 // Plan 切换时重置媒体 Tab 数据
101 useLayoutEffect(() => {
102 const previousPlanId = previousMediaResetPlanIdRef.current
103
104 if (!selectedPlanId) {
105 return
106 }
107
108 if (previousPlanId === selectedPlanId) {
109 return
110 }
111
112 previousMediaResetPlanIdRef.current = selectedPlanId
113 useMediaTabStore.getState().reset(selectedPlanId)
114 }, [selectedPlanId])
115
116 // AI 批量生成轮询
117 useGenerationPolling({
118 enabled: pollingTaskIds.length > 0,
119 taskIds: pollingTaskIds,
120 interval: 2000,
121 onTasksUpdate: syncGenerationTasks,
122 onTaskCompleted: () => {
123 if (selectedPlanId) {
124 // silentRefreshAll 内部已同步草稿数据到 planDetailStore,无需单独调用 silentRefreshMaterials
125 useMediaTabStore.getState().silentRefresh(selectedPlanId)
126 useMediaTabStore.getState().silentRefreshAll(selectedPlanId, selectedPlanId)
127 }
128 useUserStore.getState().fetchCreditsBalance()
129 },
130 onCountUpdate: updateGeneratingCount,
131 })
132
133 // 根据草稿类型计算默认选中的账户
134 const defaultAccountIds = useMemo(() => {
135 if (!publishingDraft) {
136 return undefined
137 }
138 const isVideo = publishingDraft.mediaList?.some(m => m.type === 'video')
139 const targetPubType = isVideo ? PubType.VIDEO : PubType.ImageText
140
141 const ids = accountList
142 .filter((acc) => {
143 const platConfig = getPlatformInfoSync(acc.type)
144 return isPlatformAvailable(platConfig) && platConfig.pubTypes.has(targetPubType) && acc.status !== 0
145 })
146 .map(acc => acc.id)
147 return ids
148 }, [publishingDraft, accountList])
149
150 // 发布弹框打开后预填草稿数据
151 useEffect(() => {
152 if (!publishDialogOpen || !publishingDraft)
153 return
154
155 const timer = setTimeout(async () => {
156 const store = usePublishDialog.getState()
157 if (!store.pubListChoosed?.length) {
158 return
159 }
160
161 store.setPrefillLoading(true)
162 try {
163 store.setAccountAllParams(await buildPublishParamsFromDraft(publishingDraft))
164 }
165 finally {
166 store.setPrefillLoading(false)
167 }
168 }, 500)
169
170 return () => {
171 clearTimeout(timer)
172 usePublishDialog.getState().setPrefillLoading(false)
173 }
174 }, [publishDialogOpen, publishingDraft])
175
176 // 创建草稿成功回调
177 const handleMaterialSuccess = useCallback(() => {
178 if (selectedPlanId) {
179 fetchMaterials(selectedPlanId, 1)
180 }
181 useUserStore.getState().fetchCreditsBalance()
182 }, [fetchMaterials, selectedPlanId])
183
184 return (
185 <>
186 <div className={embedded ? '@container' : undefined}>
187 <div className={embedded ? 'flex flex-col gap-4 p-4' : contentClassName}>
188 {/* AI 批量生成输入栏 */}
189 <AiBatchGenerateBar groupId={selectedPlanId || undefined} forceDraftMode={forceDraftMode} />
190 {/* 内容 Tabs:草稿箱 / 视频 / 图片 */}
191 {selectedPlanId
192 ? (
193 <DraftListSection
194 materialGroupId={selectedPlanId}
195 tabs={draftListTabs}
196 defaultTab={draftListDefaultTab}
197 allowTransfer={allowTransfer}
198 batchActionPosition={embedded ? 'sticky' : 'fixed'}
199 useContainerResponsive={embedded}
200 enablePublishDrag={enablePublishDrag}
201 />
202 )
203 : (
204 <div className="flex items-center justify-center py-12">
205 <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
206 </div>
207 )}
208 </div>
209 </div>
210
211 {/* 创建草稿弹窗 */}
212 <CreateMaterialModal
213 open={createMaterialModalOpen}
214 groupId={selectedPlanId}
215 editingMaterial={editingMaterial}
216 onClose={closeMaterialModal}
217 onSuccess={handleMaterialSuccess}
218 />
219
220 {/* 草稿详情弹窗 */}
221 <DraftDetailDialog allowTransfer={allowTransfer} />
222
223 {/* 移动到草稿箱弹窗 */}
224 {allowTransfer && <TransferDraftDialog />}
225
226 {/* 生成任务详情弹框 */}
227 <GenerationDetailDialog />
228
229 {/* 发布弹框 */}
230 {!embedded && (
231 <PublishDialog
232 open={publishDialogOpen}
233 onClose={closePublishDialog}
234 accounts={accountList}
235 defaultAccountIds={defaultAccountIds}
236 onPubSuccess={closePublishDialog}
237 />
238 )}
239
240 {/* 视频生成草稿长任务悬浮窗 */}
241 {showVideoCreateDraftTaskWidget && <VideoCreateDraftTaskWidget />}
242 </>
243 )
244 }
245
246 export default DraftContentModule
247
247 lines Plain Text