返回 AiToEarn
1 /**
2 * DraftListToolbar - 草稿列表工具栏
3 * 搜索栏 + 批量/条件删除按钮 | 批量模式:全选 + 已选数 + 取消
4 */
5
6 'use client'
7
8 import type { MaterialListFilters } from '@/api/materials/material.types'
9 import lodash from 'lodash'
10 import { ArrowRightLeft, Search, Trash2 } from 'lucide-react'
11 import { memo, useCallback, useMemo, useRef, useState } from 'react'
12 import { useShallow } from 'zustand/react/shallow'
13 import { useTransClient } from '@/app/i18n/client'
14 import { Button } from '@/components/ui/button'
15 import { Checkbox } from '@/components/ui/checkbox'
16 import { Input } from '@/components/ui/input'
17 import { usePlanDetailStore } from '@/store/draft-box/planDetailStore'
18 import { useTransferDraftDialogStore } from '@/store/draft-box/transferDraftDialogStore'
19 import { cn } from '@/utils/className'
20
21 interface DraftListToolbarProps {
22 allowTransfer?: boolean
23 useContainerResponsive?: boolean
24 }
25
26 const DraftListToolbar = memo(({ allowTransfer = true, useContainerResponsive = false }: DraftListToolbarProps) => {
27 const { t } = useTransClient('brandPromotion')
28
29 const {
30 currentPlan,
31 batchMode,
32 selectedMaterialIds,
33 materials,
34 materialsFilter,
35 setMaterialsFilter,
36 enterBatchMode,
37 exitBatchMode,
38 selectAllLoadedMaterials,
39 deselectAllMaterials,
40 openConditionalDeleteDialog,
41 } = usePlanDetailStore(
42 useShallow(state => ({
43 currentPlan: state.currentPlan,
44 batchMode: state.batchMode,
45 selectedMaterialIds: state.selectedMaterialIds,
46 materials: state.materials,
47 materialsFilter: state.materialsFilter,
48 setMaterialsFilter: state.setMaterialsFilter,
49 enterBatchMode: state.enterBatchMode,
50 exitBatchMode: state.exitBatchMode,
51 selectAllLoadedMaterials: state.selectAllLoadedMaterials,
52 deselectAllMaterials: state.deselectAllMaterials,
53 openConditionalDeleteDialog: state.openConditionalDeleteDialog,
54 })),
55 )
56
57 const openTransferDialog = useTransferDraftDialogStore(state => state.openDialog)
58
59 const [searchValue, setSearchValue] = useState(materialsFilter.title || '')
60
61 const debouncedSetFilter = useMemo(
62 () => lodash.debounce((filter: MaterialListFilters) => {
63 setMaterialsFilter(filter)
64 }, 500),
65 [setMaterialsFilter],
66 )
67
68 // 清理 debounce
69 const debouncedRef = useRef(debouncedSetFilter)
70 debouncedRef.current = debouncedSetFilter
71
72 const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
73 const value = e.target.value
74 setSearchValue(value)
75 const { materialsFilter } = usePlanDetailStore.getState()
76 debouncedRef.current({
77 ...materialsFilter,
78 title: value || undefined,
79 })
80 }, [])
81
82 const allSelected = materials.length > 0 && selectedMaterialIds.length === materials.length
83
84 const handleToggleSelectAll = useCallback(() => {
85 if (allSelected) {
86 deselectAllMaterials()
87 }
88 else {
89 selectAllLoadedMaterials()
90 }
91 }, [allSelected, deselectAllMaterials, selectAllLoadedMaterials])
92
93 const handleTransfer = useCallback(() => {
94 if (!currentPlan || selectedMaterialIds.length === 0) {
95 return
96 }
97
98 openTransferDialog({
99 currentPlanId: currentPlan.id,
100 draftIds: selectedMaterialIds,
101 mediaIds: [],
102 })
103 }, [currentPlan, openTransferDialog, selectedMaterialIds])
104
105 if (batchMode) {
106 return (
107 <div className={cn('mb-4 flex items-center gap-3', useContainerResponsive && 'flex-wrap @min-[640px]:flex-nowrap')}>
108 <div data-testid="draftbox-select-all-checkbox" className="flex items-center gap-2 cursor-pointer" onClick={handleToggleSelectAll}>
109 <Checkbox checked={allSelected} onCheckedChange={handleToggleSelectAll} />
110 <span className="text-sm">{t('draftManage.selectAll')}</span>
111 </div>
112 <span className="text-sm text-muted-foreground">
113 {t('draftManage.selectedCount', { count: selectedMaterialIds.length })}
114 </span>
115 <div className={useContainerResponsive ? 'flex-[1_1_100%] @min-[640px]:flex-1' : 'flex-1'} />
116 {allowTransfer && (
117 <Button
118 variant="outline"
119 size="sm"
120 onClick={handleTransfer}
121 disabled={selectedMaterialIds.length === 0}
122 className="cursor-pointer gap-1.5"
123 >
124 <ArrowRightLeft className="h-3.5 w-3.5" />
125 {t('draftManage.transfer')}
126 </Button>
127 )}
128 <Button data-testid="draftbox-batch-cancel-btn" variant="ghost" size="sm" onClick={exitBatchMode} className="cursor-pointer">
129 {t('draftManage.cancel')}
130 </Button>
131 </div>
132 )
133 }
134
135 return (
136 <div className={useContainerResponsive ? 'mb-4 flex flex-col gap-3 @min-[640px]:flex-row @min-[640px]:items-center' : 'flex flex-col sm:flex-row sm:items-center gap-3 mb-4'}>
137 {/* 第一行:搜索框 */}
138 <div className={useContainerResponsive ? 'relative w-full @min-[640px]:max-w-[300px]' : 'relative w-full sm:max-w-[300px]'}>
139 <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
140 <Input
141 data-testid="draftbox-search-input"
142 value={searchValue}
143 onChange={handleSearchChange}
144 placeholder={t('draftManage.searchPlaceholder')}
145 className="pl-9 h-9"
146 />
147 </div>
148 {/* 第二行:按钮组 */}
149 <div className={useContainerResponsive ? 'flex flex-wrap items-center gap-3 @min-[640px]:ml-auto' : 'flex items-center gap-3 flex-wrap sm:ml-auto'}>
150 {allowTransfer && (
151 <Button
152 variant="outline"
153 size="sm"
154 onClick={enterBatchMode}
155 className="cursor-pointer gap-1.5"
156 >
157 <ArrowRightLeft className="h-3.5 w-3.5" />
158 {t('draftManage.batchTransfer')}
159 </Button>
160 )}
161 <Button
162 data-testid="draftbox-batch-mode-btn"
163 variant="outline"
164 size="sm"
165 onClick={enterBatchMode}
166 className="cursor-pointer gap-1.5"
167 >
168 <Trash2 className="h-3.5 w-3.5" />
169 {t('draftManage.batchDelete')}
170 </Button>
171 <Button
172 data-testid="draftbox-conditional-delete-btn"
173 variant="outline"
174 size="sm"
175 onClick={openConditionalDeleteDialog}
176 className="cursor-pointer gap-1.5"
177 >
178 {t('draftManage.conditionalDelete')}
179 </Button>
180
181 </div>
182 </div>
183 )
184 })
185
186 DraftListToolbar.displayName = 'DraftListToolbar'
187
188 export { DraftListToolbar }
189
189 lines Plain Text