返回 AiToEarn
DraftListSection.tsx
根目录 / project / aitoearn-web / src / components / draft-box / components / DraftListSection.tsx
1 /**
2 * 草稿列表组件
3 * 使用 react-masonry-css 瀑布流布局 + IntersectionObserver 无限滚动展示全部草稿
4 * 图片保持原始比例显示
5 * 创建按钮卡片作为瀑布流前两个元素
6 * 支持搜索筛选、批量删除、条件删除
7 * 当传入 materialGroupId 时,集成 Tab(草稿箱/视频/图片)
8 */
9
10 'use client'
11
12 import type { PromotionMaterial } from '@/api/materials/material.types'
13 import type { PlatType } from '@/app/config/platConfig'
14 import { ArrowRightLeft, Check, ListChecks, Plus, Trash2 } from 'lucide-react'
15 import { memo, useCallback, useEffect, useMemo, useState } from 'react'
16 import Masonry from 'react-masonry-css'
17 import { useShallow } from 'zustand/react/shallow'
18 import { useTransClient } from '@/app/i18n/client'
19 import { OssImage } from '@/components/common/OssImage'
20 import { Badge } from '@/components/ui/badge'
21
22 import { Button } from '@/components/ui/button'
23 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
24 import { Skeleton } from '@/components/ui/skeleton'
25 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
26 import { usePlanDetailStore } from '@/store/draft-box/planDetailStore'
27 import { getPlatformInfoSync } from '@/store/platformMetadata'
28 import { cn } from '@/utils/className'
29 import { useContainerMasonryColumns } from '../hooks/useContainerMasonryColumns'
30 import { getMaterialUseCountLabels } from '../utils/materialUseCount'
31 import { AllListSection } from './AllListSection'
32 import { BatchActionBar } from './BatchActionBar'
33 import { ConditionalDeleteDialog } from './ConditionalDeleteDialog'
34 import { useMediaTabStore } from './ContentTabs/mediaTabStore'
35 import { DraftListToolbar } from './DraftListToolbar'
36 import {
37 GeneratingTaskCard,
38 getDraftGenerationTaskTarget,
39 shouldShowDraftGenerationTaskCard,
40 } from './GeneratingCard'
41 import { LazyImage } from './LazyImage'
42 import { LOAD_MORE_OBSERVER_OPTIONS } from './loadMoreObserver'
43 import { MediaListSection } from './MediaListSection'
44 import { PublishDialogDragSource } from './PublishDialogDragSource'
45
46 /**
47 * 瀑布流断点配置
48 */
49 const MASONRY_BREAKPOINTS = {
50 default: 5, // > 1280px
51 1280: 4, // <= 1280px
52 1024: 3, // <= 1024px
53 768: 3, // <= 768px
54 640: 2, // <= 640px
55 }
56
57 // 手动创建按钮卡片
58 const ManualCreateCard = memo(({ onClick }: { onClick: () => void }) => {
59 const { t } = useTransClient('brandPromotion')
60 return (
61 <div
62 data-testid="draftbox-manual-create-card"
63 className="mb-4 rounded-lg border-2 border-dashed border-foreground/20 bg-muted/30 overflow-hidden transition-all duration-300 hover:border-foreground/40 hover:bg-muted/50 cursor-pointer"
64 onClick={onClick}
65 >
66 <div className="flex flex-col items-center justify-center p-8 gap-4">
67 <div className="flex h-14 w-14 items-center justify-center rounded-full bg-foreground/10">
68 <Plus className="h-7 w-7 text-foreground/70" />
69 </div>
70 <span className="text-base font-medium">{t('detail.manualGenerate')}</span>
71 </div>
72 </div>
73 )
74 })
75 ManualCreateCard.displayName = 'ManualCreateCard'
76
77 // 草稿卡片 props
78 interface DraftCardProps {
79 material: PromotionMaterial
80 onClick: () => void
81 batchMode: boolean
82 selected: boolean
83 onToggleSelect: () => void
84 useCountLabels?: string[]
85 enablePublishDrag?: boolean
86 }
87
88 // 草稿卡片组件(小红书风格)
89 const DraftCard = memo(({ material, onClick, batchMode, selected, onToggleSelect, useCountLabels = [], enablePublishDrag }: DraftCardProps) => {
90 const { t } = useTransClient('brandPromotion')
91 const coverUrl = material.coverUrl || '/images/placeholder.png'
92 const canPublishDrag = !!enablePublishDrag && !batchMode
93
94 const handleClick = useCallback(() => {
95 if (batchMode) {
96 onToggleSelect()
97 }
98 else {
99 onClick()
100 }
101 }, [batchMode, onClick, onToggleSelect])
102
103 const cardNode = (
104 <div
105 data-testid="draftbox-draft-card"
106 className={cn(
107 'mb-4 cursor-pointer group relative',
108 batchMode
109 ? cn(
110 'rounded-xl transition-all duration-200',
111 selected ? 'shadow-lg' : '',
112 )
113 : '',
114 )}
115 onClick={handleClick}
116 >
117 {/* 批量模式圆形勾选指示器 */}
118 {batchMode && (
119 <div
120 data-testid="draftbox-draft-checkbox"
121 className={cn(
122 'absolute top-2 right-2 z-10 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all duration-200 shadow-sm',
123 selected
124 ? 'border-transparent bg-gradient-back scale-110'
125 : 'bg-background/90 border-muted-foreground/30 group-hover:border-primary group-hover:scale-105',
126 )}
127 onClick={(e) => { e.stopPropagation(); onToggleSelect() }}
128 >
129 {selected && <Check className="w-3.5 h-3.5 text-gradient-foreground" />}
130 </div>
131 )}
132
133 {/* 封面图 - 保持原始比例,圆角独立 */}
134 <div className="relative w-full overflow-hidden rounded-xl">
135 <LazyImage
136 src={coverUrl}
137 alt={material.title || t('material.draft')}
138 width={400}
139 height={300}
140 className="w-full h-auto transition-transform duration-300 group-hover:scale-105"
141 skeletonClassName="rounded-xl"
142 placeholderHeight={150}
143 style={{ aspectRatio: 'auto' }}
144 />
145
146 {/* hover 时显示描述遮罩 - 批量模式下隐藏 */}
147 {!batchMode && material.desc && (
148 <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-3 rounded-xl">
149 <p className="text-white text-xs line-clamp-4">
150 {material.desc}
151 </p>
152 </div>
153 )}
154
155 {/* 选中遮罩 */}
156 {batchMode && selected && (
157 <div className="absolute inset-0 bg-primary/15 pointer-events-none rounded-xl" />
158 )}
159 </div>
160 {/* 标题和模型标签 */}
161 <div className="pt-2 px-1">
162 <p className="text-sm font-medium text-foreground line-clamp-2">
163 {material.title || t('material.untitled')}
164 </p>
165 {material.model && (
166 <span className="inline-block mt-1 px-1.5 py-0.5 text-xs rounded bg-muted text-muted-foreground">
167 {material.model}
168 </span>
169 )}
170 {useCountLabels.map(label => (
171 <span
172 key={label}
173 className="inline-block mt-1 ml-1 px-1.5 py-0.5 text-xs rounded bg-primary/10 text-primary"
174 >
175 {label}
176 </span>
177 ))}
178 {material.accountTypes && material.accountTypes.length > 0 && (
179 <div className="flex flex-wrap gap-1 mt-1.5">
180 {material.accountTypes.map((type) => {
181 const platInfo = getPlatformInfoSync(type as PlatType)
182 if (!platInfo)
183 return null
184 return (
185 <OssImage
186 key={type}
187 src={platInfo.icon}
188 alt={platInfo.name}
189 width={16}
190 height={16}
191 className="w-4 h-4"
192 unoptimized
193 />
194 )
195 })}
196 </div>
197 )}
198 </div>
199 </div>
200 )
201
202 if (!canPublishDrag)
203 return cardNode
204
205 return (
206 <PublishDialogDragSource dragItem={{ kind: 'draft', material }}>
207 {cardNode}
208 </PublishDialogDragSource>
209 )
210 })
211
212 DraftCard.displayName = 'DraftCard'
213
214 // 骨架屏 - 随机高度模拟瀑布流效果(小红书风格)
215 function DraftCardSkeleton({ index }: { index: number }) {
216 // 根据 index 生成不同高度,模拟真实图片的随机比例
217 const heights = [120, 160, 200, 140, 180, 150, 170, 190]
218 const height = heights[index % heights.length]
219
220 return (
221 <div className="mb-4">
222 <Skeleton className="w-full rounded-xl" style={{ height: `${height}px` }} />
223 <div className="pt-2 px-1">
224 <Skeleton className="h-4 w-full" />
225 </div>
226 </div>
227 )
228 }
229
230 // 加载更多指示器
231 const LoadingIndicator = memo(({ label }: { label: string }) => (
232 <div className="flex justify-center py-4">
233 <div className="flex items-center gap-2 text-sm text-muted-foreground">
234 <div className="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
235 <span>{label}</span>
236 </div>
237 </div>
238 ))
239
240 LoadingIndicator.displayName = 'LoadingIndicator'
241
242 interface DraftListSectionProps {
243 materialGroupId: string
244 tabs?: DraftListSectionTab[]
245 defaultTab?: DraftListSectionTab
246 allowTransfer?: boolean
247 batchActionPosition?: 'fixed' | 'sticky'
248 useContainerResponsive?: boolean
249 enablePublishDrag?: boolean
250 }
251
252 export type DraftListSectionTab = 'all' | 'drafts' | 'video' | 'img'
253
254 const DEFAULT_DRAFT_LIST_TABS: DraftListSectionTab[] = ['all', 'drafts', 'video', 'img']
255
256 export const DraftListSection = memo(({
257 materialGroupId,
258 tabs = DEFAULT_DRAFT_LIST_TABS,
259 defaultTab = 'all',
260 allowTransfer = true,
261 batchActionPosition = 'fixed',
262 useContainerResponsive = false,
263 enablePublishDrag = false,
264 }: DraftListSectionProps) => {
265 const { t } = useTransClient('brandPromotion')
266 const { t: tMaterial } = useTransClient('material')
267 const visibleTabs = useMemo(() => tabs.length > 0 ? tabs : DEFAULT_DRAFT_LIST_TABS, [tabs])
268 const resolvedDefaultTab = visibleTabs.includes(defaultTab) ? defaultTab : visibleTabs[0]
269 const [activeTab, setActiveTab] = useState<DraftListSectionTab>(resolvedDefaultTab)
270 const hasAllTab = visibleTabs.includes('all')
271 const hasDraftsTab = visibleTabs.includes('drafts')
272 const hasVideoTab = visibleTabs.includes('video')
273 const hasImgTab = visibleTabs.includes('img')
274 const showTabsList = visibleTabs.length > 1
275 const { containerRef, masonryColumns } = useContainerMasonryColumns(useContainerResponsive)
276 const masonryBreakpointCols = useContainerResponsive ? masonryColumns : MASONRY_BREAKPOINTS
277
278 // materialGroupId 变化时,默认选中配置的 Tab
279 useEffect(() => {
280 setActiveTab(resolvedDefaultTab)
281 }, [materialGroupId, resolvedDefaultTab])
282
283 // 无限滚动加载触发器(使用 callback ref + state,确保 Tab 切换后 observer 能正确绑定)
284 const [loadMoreElement, setLoadMoreElement] = useState<HTMLDivElement | null>(null)
285 const loadMoreCallbackRef = useCallback((node: HTMLDivElement | null) => {
286 setLoadMoreElement(node)
287 }, [])
288
289 const {
290 materials,
291 materialsLoading,
292 materialsPagination,
293 generationTasks,
294 batchMode,
295 selectedMaterialIds,
296 openCreateMaterialModal,
297 loadMoreMaterials,
298 openDraftDetailDialog,
299 openGenerationDetailDialog,
300 toggleMaterialSelection,
301 fetchMaterials,
302 } = usePlanDetailStore(
303 useShallow(state => ({
304 materials: state.materials,
305 materialsLoading: state.materialsLoading,
306 materialsPagination: state.materialsPagination,
307 generationTasks: state.generationTasks,
308 batchMode: state.batchMode,
309 selectedMaterialIds: state.selectedMaterialIds,
310 openCreateMaterialModal: state.openCreateMaterialModal,
311 loadMoreMaterials: state.loadMoreMaterials,
312 openDraftDetailDialog: state.openDraftDetailDialog,
313 openGenerationDetailDialog: state.openGenerationDetailDialog,
314 toggleMaterialSelection: state.toggleMaterialSelection,
315 fetchMaterials: state.fetchMaterials,
316 })),
317 )
318
319 const { draftTotal, videoTotal, imgTotal } = useMediaTabStore(
320 useShallow(state => ({
321 draftTotal: state.all.draftTotal,
322 videoTotal: state.video.initialized ? state.video.total : state.all.videoTotal,
323 imgTotal: state.img.initialized ? state.img.total : state.all.imgTotal,
324 })),
325 )
326
327 const selectedSet = new Set(selectedMaterialIds)
328 const visibleGenerationTasks = useMemo(
329 () => generationTasks.filter(shouldShowDraftGenerationTaskCard),
330 [generationTasks],
331 )
332 const visibleDraftGenerationTasks = useMemo(
333 () => visibleGenerationTasks.filter(task => getDraftGenerationTaskTarget(task) === 'draft'),
334 [visibleGenerationTasks],
335 )
336 const visibleVideoGenerationTasks = useMemo(
337 () => visibleGenerationTasks.filter(task => getDraftGenerationTaskTarget(task) === 'video'),
338 [visibleGenerationTasks],
339 )
340 const visibleImageGenerationTasks = useMemo(
341 () => visibleGenerationTasks.filter(task => getDraftGenerationTaskTarget(task) === 'img'),
342 [visibleGenerationTasks],
343 )
344 const allTotal = draftTotal + videoTotal + imgTotal + visibleGenerationTasks.length
345
346 const exitMediaBatchMode = useMediaTabStore(state => state.exitBatchMode)
347 const enterMediaBatchMode = useMediaTabStore(state => state.enterBatchMode)
348 const mediaBatchMode = useMediaTabStore(state => state.batchMode)
349 const exitDraftBatchMode = usePlanDetailStore(state => state.exitBatchMode)
350
351 // Tab 切换处理
352 const handleTabChange = useCallback((value: string) => {
353 if (!visibleTabs.includes(value as DraftListSectionTab))
354 return
355
356 // 切换 Tab 时退出所有批量模式
357 exitDraftBatchMode()
358 exitMediaBatchMode()
359
360 setActiveTab(value as DraftListSectionTab)
361
362 // 草稿 Tab 无独立列表组件,仍在这里兜底首次加载;媒体/全部 Tab 交由各自内容组件加载。
363 if (value === 'drafts' && !usePlanDetailStore.getState().materialsInitialized && materialGroupId) {
364 fetchMaterials(materialGroupId, 1)
365 }
366 }, [materialGroupId, exitDraftBatchMode, exitMediaBatchMode, fetchMaterials, visibleTabs])
367
368 // IntersectionObserver 实现无限滚动
369 useEffect(() => {
370 if (!loadMoreElement)
371 return
372
373 const observer = new IntersectionObserver(
374 (entries) => {
375 const [entry] = entries
376 if (entry.isIntersecting && materialsPagination.hasMore && !materialsLoading && materialGroupId) {
377 loadMoreMaterials(materialGroupId)
378 }
379 },
380 LOAD_MORE_OBSERVER_OPTIONS,
381 )
382
383 observer.observe(loadMoreElement)
384
385 return () => {
386 observer.disconnect()
387 }
388 }, [loadMoreElement, materialsPagination.hasMore, materialsLoading, materialGroupId, loadMoreMaterials])
389
390 // 根据 activeTab 获取标题
391 const getHeaderTitle = () => {
392 switch (activeTab) {
393 case 'all':
394 return tMaterial('mediaManagement.all')
395 case 'video':
396 return tMaterial('mediaManagement.video')
397 case 'img':
398 return tMaterial('mediaManagement.image')
399 default:
400 return tMaterial('mediaManagement.drafts', '草稿')
401 }
402 }
403
404 const getActiveTabCount = () => {
405 switch (activeTab) {
406 case 'all':
407 return allTotal
408 case 'drafts':
409 return materialsPagination.total + visibleDraftGenerationTasks.length
410 case 'video':
411 return videoTotal + visibleVideoGenerationTasks.length
412 case 'img':
413 return imgTotal + visibleImageGenerationTasks.length
414 default:
415 return materialsPagination.total + visibleDraftGenerationTasks.length
416 }
417 }
418
419 const isDraftsTab = activeTab === 'drafts'
420 const showMediaHeaderBatchActions = !isDraftsTab && !mediaBatchMode && getActiveTabCount() > 0
421 const cardContentClassName = useContainerResponsive
422 ? 'px-4 pb-4 @min-[640px]:px-6 @min-[640px]:pb-6'
423 : 'px-4 pb-4 sm:px-6 sm:pb-6'
424 const tabCountBadgeClassName = 'ml-1 h-5 min-w-[20px] px-1.5 text-xs'
425 const headerCountBadgeClassName = showTabsList ? 'hidden' : 'h-5 min-w-[20px] px-1.5 text-xs'
426 const batchCardContentClassName = cn(
427 cardContentClassName,
428 (batchMode || mediaBatchMode) && 'pb-16',
429 )
430
431 // 草稿内容区域
432 const draftsContent = (
433 <>
434 <DraftListToolbar allowTransfer={allowTransfer} useContainerResponsive={useContainerResponsive} />
435 <div>
436 <Masonry
437 breakpointCols={masonryBreakpointCols}
438 className="flex -ml-4 w-auto"
439 columnClassName="pl-4 bg-clip-padding"
440 data-testid="draftbox-masonry-list"
441 >
442 {!batchMode && (
443 <ManualCreateCard onClick={openCreateMaterialModal} />
444 )}
445
446 {/* 生成中卡片 - 批量模式隐藏 */}
447 {!batchMode && visibleDraftGenerationTasks.map(task => (
448 <GeneratingTaskCard key={task.id} task={task} onClick={openGenerationDetailDialog} />
449 ))}
450
451 {/* 草稿数据 */}
452 {materials.map(material => (
453 <DraftCard
454 key={material.id}
455 material={material}
456 onClick={() => openDraftDetailDialog(material)}
457 batchMode={batchMode}
458 selected={selectedSet.has(material.id)}
459 onToggleSelect={() => toggleMaterialSelection(material.id)}
460 useCountLabels={getMaterialUseCountLabels(material, t)}
461 enablePublishDrag={enablePublishDrag}
462 />
463 ))}
464 </Masonry>
465
466 {/* 加载触发器 */}
467 <div ref={loadMoreCallbackRef} />
468
469 {/* 加载更多指示器 */}
470 {materialsLoading && <LoadingIndicator label={t('common.loading')} />}
471
472 {/* 没有更多数据 */}
473 {!materialsPagination.hasMore && materials.length > 0 && (
474 <div className="flex items-center justify-center py-4">
475 <span className="text-sm text-muted-foreground">
476 {t('common.noMore', '没有更多了')}
477 </span>
478 </div>
479 )}
480 </div>
481 </>
482 )
483
484 // 骨架屏内容
485 // TabsList 组件
486 const tabsList = showTabsList ? (
487 <TabsList className={useContainerResponsive ? 'grid h-auto w-full grid-cols-4 gap-1 @min-[640px]:inline-flex @min-[640px]:w-auto' : 'grid h-auto w-full grid-cols-4 gap-1 sm:inline-flex sm:w-auto'}>
488 {hasAllTab && (
489 <TabsTrigger value="all" className={useContainerResponsive ? 'min-w-0 cursor-pointer px-2 text-xs @min-[640px]:px-3 @min-[640px]:text-sm' : 'cursor-pointer min-w-0 px-2 text-xs sm:px-3 sm:text-sm'}>
490 {tMaterial('mediaManagement.all')}
491 {allTotal > 0 && (
492 <Badge variant="secondary" className={tabCountBadgeClassName}>
493 {allTotal}
494 </Badge>
495 )}
496 </TabsTrigger>
497 )}
498 {hasDraftsTab && (
499 <TabsTrigger value="drafts" className={useContainerResponsive ? 'min-w-0 cursor-pointer px-2 text-xs @min-[640px]:px-3 @min-[640px]:text-sm' : 'cursor-pointer min-w-0 px-2 text-xs sm:px-3 sm:text-sm'}>
500 {tMaterial('mediaManagement.drafts', '草稿')}
501 {materialsPagination.total > 0 && (
502 <Badge variant="secondary" className={tabCountBadgeClassName}>
503 {materialsPagination.total}
504 </Badge>
505 )}
506 </TabsTrigger>
507 )}
508 {hasVideoTab && (
509 <TabsTrigger value="video" className={useContainerResponsive ? 'min-w-0 cursor-pointer px-2 text-xs @min-[640px]:px-3 @min-[640px]:text-sm' : 'cursor-pointer min-w-0 px-2 text-xs sm:px-3 sm:text-sm'}>
510 {tMaterial('mediaManagement.video')}
511 {videoTotal > 0 && (
512 <Badge variant="secondary" className={tabCountBadgeClassName}>
513 {videoTotal}
514 </Badge>
515 )}
516 </TabsTrigger>
517 )}
518 {hasImgTab && (
519 <TabsTrigger value="img" className={useContainerResponsive ? 'min-w-0 cursor-pointer px-2 text-xs @min-[640px]:px-3 @min-[640px]:text-sm' : 'cursor-pointer min-w-0 px-2 text-xs sm:px-3 sm:text-sm'}>
520 {tMaterial('mediaManagement.image')}
521 {imgTotal > 0 && (
522 <Badge variant="secondary" className={tabCountBadgeClassName}>
523 {imgTotal}
524 </Badge>
525 )}
526 </TabsTrigger>
527 )}
528 </TabsList>
529 ) : null
530
531 // 初始加载骨架屏
532 if (materialsLoading && materials.length === 0) {
533 return (
534 <Tabs ref={useContainerResponsive ? containerRef : undefined} value={activeTab} onValueChange={handleTabChange} className="w-full">
535 <Card>
536 {tabsList && (
537 <div className={useContainerResponsive ? 'px-4 pt-4 @min-[640px]:px-6 @min-[640px]:pt-6' : 'px-4 pt-4 sm:px-6 sm:pt-6'}>
538 {tabsList}
539 </div>
540 )}
541 <CardHeader className={useContainerResponsive ? 'px-4 pb-4 pt-4 @min-[640px]:px-6' : 'px-4 pb-4 pt-4 sm:px-6'}>
542 <div className="flex items-center justify-between gap-3">
543 <div className="flex min-w-0 flex-wrap items-center gap-2">
544 <CardTitle className="text-base">{getHeaderTitle()}</CardTitle>
545 {getActiveTabCount() > 0 && (
546 <Badge variant="secondary" className={headerCountBadgeClassName}>
547 {getActiveTabCount()}
548 </Badge>
549 )}
550 </div>
551 <Button
552 variant="ghost"
553 size="sm"
554 onClick={openGenerationDetailDialog}
555 className="shrink-0 cursor-pointer justify-center gap-1.5 text-muted-foreground"
556 >
557 <ListChecks className="h-3.5 w-3.5" />
558 {t('draftManage.generationDetail')}
559 </Button>
560 </div>
561 </CardHeader>
562 <CardContent className={cardContentClassName}>
563 <Masonry
564 breakpointCols={masonryBreakpointCols}
565 className="flex -ml-4 w-auto"
566 columnClassName="pl-4 bg-clip-padding"
567 >
568 <ManualCreateCard onClick={openCreateMaterialModal} />
569 {Array.from({ length: 8 }).map((_, i) => (
570 <DraftCardSkeleton key={i} index={i} />
571 ))}
572 </Masonry>
573 </CardContent>
574 </Card>
575 </Tabs>
576 )
577 }
578 return (
579 <Tabs ref={useContainerResponsive ? containerRef : undefined} value={activeTab} onValueChange={handleTabChange} className="w-full">
580 <Card>
581 {tabsList && (
582 <div className={useContainerResponsive ? 'px-4 pt-4 @min-[640px]:px-6 @min-[640px]:pt-6' : 'px-4 pt-4 sm:px-6 sm:pt-6'}>
583 {tabsList}
584 </div>
585 )}
586 <CardHeader className={useContainerResponsive ? 'px-4 pb-4 pt-4 @min-[640px]:px-6' : 'px-4 pb-4 pt-4 sm:px-6'}>
587 <div className={useContainerResponsive ? 'flex flex-col gap-3 @min-[640px]:flex-row @min-[640px]:items-center @min-[640px]:justify-between' : 'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'}>
588 <div className={useContainerResponsive ? 'flex items-center justify-between gap-3 @min-[640px]:min-w-0 @min-[640px]:flex-1 @min-[640px]:justify-start' : 'flex items-center justify-between gap-3 sm:min-w-0 sm:flex-1 sm:justify-start'}>
589 <div className={useContainerResponsive ? 'flex min-w-0 flex-wrap items-center gap-2' : 'flex min-w-0 flex-wrap items-center gap-2'}>
590 <CardTitle className="text-base">{getHeaderTitle()}</CardTitle>
591 {getActiveTabCount() > 0 && (
592 <Badge variant="secondary" className={headerCountBadgeClassName}>
593 {getActiveTabCount()}
594 </Badge>
595 )}
596 </div>
597 <Button
598 variant="ghost"
599 size="sm"
600 onClick={openGenerationDetailDialog}
601 className={useContainerResponsive ? 'shrink-0 cursor-pointer justify-center gap-1.5 text-muted-foreground @min-[640px]:hidden' : 'shrink-0 cursor-pointer justify-center gap-1.5 text-muted-foreground sm:hidden'}
602 >
603 <ListChecks className="h-3.5 w-3.5" />
604 {t('draftManage.generationDetail')}
605 </Button>
606 </div>
607 <div className={useContainerResponsive ? 'flex w-full flex-col gap-2 @min-[640px]:w-auto @min-[640px]:flex-row @min-[640px]:items-center @min-[640px]:justify-end' : 'flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end'}>
608 {showMediaHeaderBatchActions && (
609 <div className={useContainerResponsive ? 'grid w-full grid-cols-2 gap-2 @min-[640px]:flex @min-[640px]:w-auto' : 'grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto'}>
610 <Button
611 variant="outline"
612 size="sm"
613 onClick={enterMediaBatchMode}
614 className={useContainerResponsive ? 'w-full cursor-pointer justify-center gap-1.5 @min-[640px]:w-auto' : 'w-full cursor-pointer justify-center gap-1.5 sm:w-auto'}
615 >
616 <ArrowRightLeft className="h-3.5 w-3.5" />
617 {t('draftManage.batchTransfer')}
618 </Button>
619 <Button
620 variant="outline"
621 size="sm"
622 onClick={enterMediaBatchMode}
623 className={useContainerResponsive ? 'w-full cursor-pointer justify-center gap-1.5 @min-[640px]:w-auto' : 'w-full cursor-pointer justify-center gap-1.5 sm:w-auto'}
624 >
625 <Trash2 className="h-3.5 w-3.5" />
626 {tMaterial('mediaManagement.batchDelete')}
627 </Button>
628 </div>
629 )}
630 <Button
631 variant="ghost"
632 size="sm"
633 onClick={openGenerationDetailDialog}
634 className={useContainerResponsive ? 'hidden cursor-pointer justify-center gap-1.5 text-muted-foreground @min-[640px]:inline-flex' : 'hidden cursor-pointer justify-center gap-1.5 text-muted-foreground sm:inline-flex'}
635 >
636 <ListChecks className="h-3.5 w-3.5" />
637 {t('draftManage.generationDetail')}
638 </Button>
639 </div>
640 </div>
641 </CardHeader>
642
643 {hasAllTab && (
644 <TabsContent value="all" className="mt-0">
645 <CardContent className={batchCardContentClassName}>
646 <AllListSection materialGroupId={materialGroupId} showBatchDeleteTrigger={false} batchActionPosition={batchActionPosition} useContainerResponsive={useContainerResponsive} enablePublishDrag={enablePublishDrag} />
647 </CardContent>
648 </TabsContent>
649 )}
650
651 {hasDraftsTab && (
652 <TabsContent value="drafts" className="mt-0">
653 <CardContent className={batchCardContentClassName}>
654 {draftsContent}
655 </CardContent>
656 </TabsContent>
657 )}
658
659 {hasVideoTab && (
660 <TabsContent value="video" className="mt-0">
661 <CardContent className={batchCardContentClassName}>
662 <MediaListSection
663 type="video"
664 materialGroupId={materialGroupId}
665 showBatchDeleteTrigger={false}
666 batchActionPosition={batchActionPosition}
667 useContainerResponsive={useContainerResponsive}
668 enablePublishDrag={enablePublishDrag}
669 />
670 </CardContent>
671 </TabsContent>
672 )}
673
674 {hasImgTab && (
675 <TabsContent value="img" className="mt-0">
676 <CardContent className={batchCardContentClassName}>
677 <MediaListSection
678 type="img"
679 materialGroupId={materialGroupId}
680 showBatchDeleteTrigger={false}
681 batchActionPosition={batchActionPosition}
682 useContainerResponsive={useContainerResponsive}
683 enablePublishDrag={enablePublishDrag}
684 />
685 </CardContent>
686 </TabsContent>
687 )}
688
689 {activeTab === 'drafts' && batchMode && (
690 <BatchActionBar allowTransfer={allowTransfer} position={batchActionPosition} useContainerResponsive={useContainerResponsive} />
691 )}
692 <ConditionalDeleteDialog />
693 </Card>
694 </Tabs>
695 )
696 })
697
698 DraftListSection.displayName = 'DraftListSection'
699
699 lines Plain Text