| 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 |