返回 AiToEarn
1 /**
2 * CreateMaterialModal - 创建/编辑素材弹窗
3 * 简化版本:只需要标题、描述、上传资源三个字段
4 * 桌面端使用 PubParmasTextarea 组件,移动端使用独立全屏布局
5 */
6 'use client'
7
8 import type { PromotionMaterial } from '@/api/materials/material.types'
9 import { Bot } from 'lucide-react'
10 import { useRouter } from 'next/navigation'
11 import { memo } from 'react'
12 import { useTranslation } from 'react-i18next'
13 import PubParmasTextarea from '@/components/PublishDialog/compoents/PubParmasTextarea'
14 import { Button } from '@/components/ui/button'
15 import {
16 Dialog,
17 DialogContent,
18 DialogFooter,
19 DialogHeader,
20 DialogTitle,
21 } from '@/components/ui/dialog'
22 import { Input } from '@/components/ui/input'
23 import { useIsMobile } from '@/hooks/useIsMobile'
24 import { cn } from '@/utils/className'
25 import InlinePlatformSelector from './InlinePlatformSelector'
26 import { MaterialValidationAlert } from './MaterialValidationAlert'
27 import MobileContent from './MobileContent'
28 import { useCreateMaterialForm } from './useCreateMaterialForm'
29 import { useMaterialValidation } from './useMaterialValidation'
30 import { getMaterialUploadPlatType } from './utils'
31
32 export interface CreateMaterialModalProps {
33 open: boolean
34 /** 素材组 ID(草稿箱关联的素材组) */
35 groupId: string | null
36 /** 编辑模式下的素材数据 */
37 editingMaterial?: PromotionMaterial | null
38 /** 是否正在提交 */
39 isSubmitting?: boolean
40 /** 关闭弹窗 */
41 onClose: () => void
42 /** 创建/更新成功回调 */
43 onSuccess?: () => void
44 }
45
46 /**
47 * 创建/编辑素材弹窗内容(桌面端)
48 */
49 const CreateMaterialModalContent = memo(
50 ({
51 groupId,
52 editingMaterial,
53 isSubmitting: externalSubmitting,
54 onClose,
55 onSuccess,
56 }: Omit<CreateMaterialModalProps, 'open'>) => {
57 const { t } = useTranslation('brandPromotion')
58 const router = useRouter()
59
60 const {
61 params,
62 updateParams,
63 validationIssues,
64 isSubmitting: submitting,
65 handleSubmit,
66 } = useCreateMaterialForm({
67 groupId,
68 editingMaterial,
69 isSubmitting: externalSubmitting,
70 onClose,
71 onSuccess,
72 })
73
74 const { effectiveLimits } = useMaterialValidation(params.selectedPlatforms)
75 const uploadPlatType = getMaterialUploadPlatType(params.selectedPlatforms)
76
77 return (
78 <>
79 <DialogHeader>
80 <DialogTitle>{t('createMaterial.title')}</DialogTitle>
81 </DialogHeader>
82
83 <div className="flex max-h-[70vh] flex-col gap-4 overflow-y-auto py-4 pr-1">
84 {/* 平台选择器 */}
85 <div className="px-1">
86 <InlinePlatformSelector
87 selectedPlatforms={params.selectedPlatforms}
88 onPlatformsChange={platforms => updateParams({ selectedPlatforms: platforms })}
89 />
90 </div>
91
92 {/* 提交后平台兼容性问题 */}
93 {validationIssues.length > 0 && (
94 <div className="px-1">
95 <MaterialValidationAlert issues={validationIssues} />
96 </div>
97 )}
98
99 {/* 使用 PubParmasTextarea 组件 */}
100 <div className="px-1">
101 <PubParmasTextarea
102 platType={uploadPlatType}
103 desValue={params.des}
104 imageFileListValue={params.images}
105 videoFileValue={params.video}
106 disableUploadCompatibilityCheck
107 onChange={({ value, imgs, video }) => {
108 updateParams({ des: value, images: imgs || [], video })
109 }}
110 toolbarExtra={(
111 <div className="px-1.5 border-l border-border first:border-l-0">
112 <Button
113 variant="ghost"
114 size="sm"
115 className="cursor-pointer transition-all hover:bg-gradient-to-r hover:from-purple-500/10 hover:to-blue-500/10"
116 onClick={(e) => {
117 e.stopPropagation()
118 router.push(
119 `/ai-social?agentExternalPrompt=${encodeURIComponent(t('detail.agentGeneratePrompt'))}`,
120 )
121 }}
122 >
123 <Bot className="mr-1 h-4 w-4" />
124 {t('detail.agentGenerate')}
125 </Button>
126 </div>
127 )}
128 extend={(
129 <div className="flex items-center h-10">
130 <label className="shrink-0 w-[60px] text-sm">
131 {t('createMaterial.titleLabel')}
132 </label>
133 <Input
134 data-testid="draftbox-material-title-input"
135 value={params.title}
136 placeholder={t('createMaterial.titlePlaceholder')}
137 onChange={e => updateParams({ title: e.target.value })}
138 />
139 {effectiveLimits.titleMax && (
140 <span
141 className={cn(
142 'shrink-0 ml-2 text-xs tabular-nums',
143 params.title.length > effectiveLimits.titleMax.value
144 ? 'text-destructive'
145 : 'text-muted-foreground',
146 )}
147 >
148 {params.title.length}
149 /
150 {effectiveLimits.titleMax.value}
151 </span>
152 )}
153 </div>
154 )}
155 />
156 </div>
157 </div>
158
159 <DialogFooter>
160 <Button
161 variant="outline"
162 onClick={onClose}
163 disabled={submitting}
164 className="cursor-pointer"
165 >
166 {t('common.cancel')}
167 </Button>
168 <Button
169 data-testid="draftbox-material-save-btn"
170 onClick={handleSubmit}
171 disabled={submitting}
172 className="cursor-pointer"
173 >
174 {submitting ? t('common.loading') : t('common.confirm')}
175 </Button>
176 </DialogFooter>
177 </>
178 )
179 },
180 )
181
182 CreateMaterialModalContent.displayName = 'CreateMaterialModalContent'
183
184 /**
185 * 创建素材弹窗
186 */
187 export const CreateMaterialModal = memo(({ open, ...props }: CreateMaterialModalProps) => {
188 const isMobile = useIsMobile()
189
190 if (!open)
191 return null
192
193 return (
194 <Dialog open={open} onOpenChange={isOpen => !isOpen && props.onClose()}>
195 <DialogContent
196 data-testid="draftbox-create-material-modal"
197 className={cn(
198 isMobile
199 ? '!left-0 !top-0 !translate-x-0 !translate-y-0 !w-full !h-[100dvh] !max-w-none !rounded-none !p-0 !border-none !gap-0 flex flex-col'
200 : 'sm:max-w-[700px]',
201 )}
202 hideCloseButton={isMobile}
203 >
204 {isMobile ? <MobileContent {...props} /> : <CreateMaterialModalContent {...props} />}
205 </DialogContent>
206 </Dialog>
207 )
208 })
209
210 CreateMaterialModal.displayName = 'CreateMaterialModal'
211
212 export default CreateMaterialModal
213
213 lines Plain Text