返回 AiToEarn
1 /**
2 * ConditionalDeleteDialog - 按条件删除草稿弹窗
3 * 外层控制渲染,内层使用 hooks(避免 useTransClient 动态加载闪烁)
4 */
5
6 'use client'
7
8 import type { MaterialListFilters } from '@/api/materials/material.types'
9 import lodash from 'lodash'
10 import { Loader2 } from 'lucide-react'
11 import { memo, useCallback, useEffect, useMemo, useState } from 'react'
12 import { useShallow } from 'zustand/react/shallow'
13 import { apiGetMaterialList } from '@/api/materials/material.api'
14 import { useTransClient } from '@/app/i18n/client'
15 import { Button } from '@/components/ui/button'
16 import {
17 Dialog,
18 DialogContent,
19 DialogFooter,
20 DialogHeader,
21 DialogTitle,
22 } from '@/components/ui/dialog'
23 import { Input } from '@/components/ui/input'
24 import { Label } from '@/components/ui/label'
25 import { NumberInput } from '@/components/ui/number-input'
26 import { usePlanDetailStore } from '@/store/draft-box/planDetailStore'
27 import { toast } from '@/utils/ui/toast'
28
29 // 外层:控制渲染时机
30 const ConditionalDeleteDialog = memo(() => {
31 const { open, closeDialog } = usePlanDetailStore(
32 useShallow(state => ({
33 open: state.conditionalDeleteDialogOpen,
34 closeDialog: state.closeConditionalDeleteDialog,
35 })),
36 )
37
38 if (!open)
39 return null
40
41 return (
42 <ConditionalDeleteDialogContent onOpenChange={(v) => {
43 if (!v)
44 closeDialog()
45 }}
46 />
47 )
48 })
49
50 ConditionalDeleteDialog.displayName = 'ConditionalDeleteDialog'
51
52 // 内层:使用 hooks
53 const ConditionalDeleteDialogContent = memo(({ onOpenChange }: { onOpenChange: (open: boolean) => void }) => {
54 const { t } = useTransClient('brandPromotion')
55
56 const { currentPlan, filterDeleteMaterials } = usePlanDetailStore(
57 useShallow(state => ({
58 currentPlan: state.currentPlan,
59 filterDeleteMaterials: state.filterDeleteMaterials,
60 })),
61 )
62
63 const [title, setTitle] = useState('')
64 const [useCount, setUseCount] = useState<number | undefined>()
65 const [matchCount, setMatchCount] = useState<number | null>(null)
66 const [querying, setQuerying] = useState(false)
67 const [deleting, setDeleting] = useState(false)
68
69 const hasCondition = title.trim() !== '' || useCount !== undefined
70
71 // 构建筛选条件
72 const buildFilters = useCallback((): MaterialListFilters => {
73 const filters: MaterialListFilters = {}
74 if (title.trim())
75 filters.title = title.trim()
76 if (useCount !== undefined)
77 filters.useCount = useCount
78 return filters
79 }, [title, useCount])
80
81 // debounce 查询匹配数量
82 const queryMatchCount = useMemo(
83 () => lodash.debounce(async (groupId: string, filters: MaterialListFilters) => {
84 if (!filters.title && filters.useCount === undefined) {
85 setMatchCount(null)
86 setQuerying(false)
87 return
88 }
89 setQuerying(true)
90 try {
91 const res = await apiGetMaterialList(groupId, 1, 0, filters)
92 setMatchCount(res?.data?.total ?? 0)
93 }
94 catch {
95 setMatchCount(null)
96 }
97 finally {
98 setQuerying(false)
99 }
100 }, 500),
101 [],
102 )
103
104 useEffect(() => {
105 if (!currentPlan)
106 return
107 const filters = buildFilters()
108 if (!filters.title && filters.useCount === undefined) {
109 setMatchCount(null)
110 return
111 }
112 setQuerying(true)
113 queryMatchCount(currentPlan.id, filters)
114 }, [title, useCount, currentPlan, buildFilters, queryMatchCount])
115
116 // 清理 debounce
117 useEffect(() => {
118 return () => {
119 queryMatchCount.cancel()
120 }
121 }, [queryMatchCount])
122
123 const handleDelete = useCallback(async () => {
124 if (!hasCondition || matchCount === 0)
125 return
126 setDeleting(true)
127 try {
128 const conditions = buildFilters()
129 const success = await filterDeleteMaterials(conditions)
130 if (success) {
131 toast.success(t('draftManage.conditionalDeleteSuccess'))
132 }
133 else {
134 toast.error(t('draftManage.conditionalDeleteFailed'))
135 }
136 }
137 finally {
138 setDeleting(false)
139 }
140 }, [hasCondition, matchCount, buildFilters, filterDeleteMaterials, t])
141
142 const deleteDisabled = !hasCondition || matchCount === 0 || matchCount === null || deleting
143
144 return (
145 <Dialog open onOpenChange={onOpenChange}>
146 <DialogContent data-testid="draftbox-cond-delete-dialog" className="max-w-[420px]">
147 <DialogHeader>
148 <DialogTitle>{t('draftManage.conditionalDeleteTitle')}</DialogTitle>
149 </DialogHeader>
150
151 <div className="space-y-4 py-2">
152 <div className="space-y-2">
153 <Label>{t('draftManage.conditionTitle')}</Label>
154 <Input
155 data-testid="draftbox-cond-title-input"
156 value={title}
157 onChange={e => setTitle(e.target.value)}
158 placeholder={t('draftManage.conditionTitlePlaceholder')}
159 />
160 </div>
161
162 <div className="space-y-2">
163 <Label>{t('draftManage.conditionUseCount')}</Label>
164 <NumberInput
165 data-testid="draftbox-cond-usecount-input"
166 value={useCount}
167 onValueChange={v => setUseCount(v)}
168 decimalScale={0}
169 allowNegative={false}
170 placeholder={t('draftManage.conditionUseCountPlaceholder')}
171 />
172 </div>
173
174 <div data-testid="draftbox-cond-match-count" className="rounded-md bg-muted p-3 text-sm">
175 {!hasCondition && (
176 <span className="text-muted-foreground">{t('draftManage.setConditionHint')}</span>
177 )}
178 {hasCondition && querying && (
179 <span className="text-muted-foreground flex items-center gap-2">
180 <Loader2 className="h-3.5 w-3.5 animate-spin" />
181 {t('draftManage.matchCountLoading')}
182 </span>
183 )}
184 {hasCondition && !querying && matchCount !== null && (
185 <span className={matchCount > 0 ? 'text-destructive font-medium' : 'text-muted-foreground'}>
186 {t('draftManage.matchCount', { count: matchCount })}
187 </span>
188 )}
189 </div>
190 </div>
191
192 <DialogFooter>
193 <Button variant="ghost" onClick={() => onOpenChange(false)} className="cursor-pointer">
194 {t('draftManage.cancel')}
195 </Button>
196 <Button
197 data-testid="draftbox-cond-delete-confirm-btn"
198 variant="destructive"
199 onClick={handleDelete}
200 disabled={deleteDisabled}
201 className="cursor-pointer gap-1.5"
202 >
203 {deleting && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
204 {t('common.delete')}
205 {matchCount !== null && matchCount > 0 && ` (${matchCount})`}
206 </Button>
207 </DialogFooter>
208 </DialogContent>
209 </Dialog>
210 )
211 })
212
213 ConditionalDeleteDialogContent.displayName = 'ConditionalDeleteDialogContent'
214
215 export { ConditionalDeleteDialog }
216
216 lines Plain Text