返回 AiToEarn
usePublishDialog.ts
根目录 / project / aitoearn-web / src / components / PublishDialog / usePublishDialog.ts
1 import type { SocialAccount } from '@/api/accounts/account.types'
2 import type { ErrPubParamsMapType } from '@/components/PublishDialog/hooks/usePubParamsVerify'
3 import type { IPlatOption, IPubParams, PubItem } from '@/components/PublishDialog/publishDialog.type'
4 import lodash from 'lodash'
5 import { create } from 'zustand'
6 import { combine } from 'zustand/middleware'
7 import { AccountStatus } from '@/app/config/accountConfig'
8 import { PlatType } from '@/app/config/platConfig'
9 import { PubType } from '@/app/config/publishConfig'
10 import { getSocialAccountIdentityKeys, isPublishTitleSupported, isSameSocialAccount } from '@/components/PublishDialog/PublishDialog.util'
11 import { usePublishDialogStorageStore } from '@/components/PublishDialog/usePublishDialogStorageStore'
12 import { getPlatformInfoSync, isPlatformEnabledSync } from '@/store/platformMetadata'
13
14 export interface IPublishDialogStore {
15 // 选择的发布列表
16 pubListChoosed: PubItem[]
17 // 所有发布列表
18 pubList: PubItem[]
19 // 通用发布参数
20 commonPubParams: IPubParams
21 // 当前步骤,0=所有账号没有参数,要设置参数。 1=所有账号有参数,详细设置参数
22 step: number
23 // 第二步时需要,展开的账户参数
24 expandedPubItem?: PubItem
25 // 错误提示
26 errParamsMap?: ErrPubParamsMapType
27 warningParamsMap?: ErrPubParamsMapType
28 // 发布时间,为空则为立即发布
29 pubTime?: string
30 // 预填内容加载中
31 prefillLoading: boolean
32 // 发布内容添加加载中(草稿/素材拖拽导入)
33 publishContentLoading: boolean
34 }
35
36 const store: IPublishDialogStore = {
37 pubListChoosed: [],
38 pubTime: undefined,
39 pubList: [],
40 step: 0,
41 commonPubParams: {
42 title: '',
43 des: '',
44 video: undefined,
45 images: [],
46 option: {
47 bilibili: {
48 tid: undefined,
49 copyright: 1,
50 source: '',
51 },
52 facebook: {
53 content_category: undefined,
54 },
55 xhs: {},
56 instagram: {
57 content_category: undefined,
58 },
59 },
60 },
61 expandedPubItem: undefined,
62 errParamsMap: undefined,
63 warningParamsMap: undefined,
64 prefillLoading: false,
65 publishContentLoading: false,
66 }
67
68 interface SyncAccountsOptions {
69 applyDefaultWhenEmpty?: boolean
70 }
71
72 function getStore() {
73 return lodash.cloneDeep(store)
74 }
75
76 function getDefaultContentCategory(params: Pick<IPubParams, 'video'>) {
77 return params.video ? 'reel' : 'post'
78 }
79
80 function getNormalizedContentCategory(currentCategory: string | undefined, params: Pick<IPubParams, 'video'>) {
81 if (params.video)
82 return 'reel'
83
84 return currentCategory || getDefaultContentCategory(params)
85 }
86
87 function getVisiblePublishAccounts(account: SocialAccount[]) {
88 return account.filter(v => isPlatformEnabledSync(v.type))
89 }
90
91 function dedupePublishAccounts(accounts: SocialAccount[]) {
92 return accounts.reduce<SocialAccount[]>((result, accountItem) => {
93 const existingIndex = result.findIndex(item => isSameSocialAccount(item, accountItem))
94 if (existingIndex === -1) {
95 result.push(accountItem)
96 return result
97 }
98
99 result[existingIndex] = accountItem
100 return result
101 }, [])
102 }
103
104 function normalizePlatformOptions(accountType: SocialAccount['type'], params: IPubParams): IPubParams {
105 const option = { ...params.option }
106 const nextParams = { ...params }
107
108 if (!isPublishTitleSupported(accountType))
109 delete nextParams.title
110
111 if (accountType === PlatType.Instagram) {
112 option.instagram = {
113 ...option.instagram,
114 content_category: getNormalizedContentCategory(option.instagram?.content_category, params),
115 }
116 }
117
118 if (accountType === PlatType.Facebook) {
119 option.facebook = {
120 ...option.facebook,
121 content_category: getNormalizedContentCategory(option.facebook?.content_category, params),
122 }
123 }
124
125 return {
126 ...nextParams,
127 option,
128 }
129 }
130
131 function isRecord(value: unknown): value is Record<string, unknown> {
132 return typeof value === 'object' && value !== null && !Array.isArray(value)
133 }
134
135 function removeUndefinedFields(target: Record<string, unknown>, patch: Record<string, unknown>) {
136 for (const fieldKey of Object.keys(patch)) {
137 if (patch[fieldKey] === undefined) {
138 delete target[fieldKey]
139 continue
140 }
141
142 const patchValue = patch[fieldKey]
143 const targetValue = target[fieldKey]
144 if (isRecord(patchValue) && isRecord(targetValue)) {
145 removeUndefinedFields(targetValue, patchValue)
146 }
147 }
148 }
149
150 function removeUndefinedOptionFields(target: IPlatOption, patch: IPlatOption) {
151 for (const platKey of Object.keys(patch) as Array<keyof IPlatOption>) {
152 const platPatch = patch[platKey]
153
154 if (platPatch === undefined) {
155 delete target[platKey]
156 continue
157 }
158
159 const platTarget = target[platKey]
160 const patchRecord: unknown = platPatch
161 const targetRecord: unknown = platTarget
162 if (!isRecord(patchRecord) || !isRecord(targetRecord))
163 continue
164
165 removeUndefinedFields(targetRecord, patchRecord)
166 }
167 }
168
169 function normalizePubItem(pubItem: PubItem): PubItem {
170 return {
171 ...pubItem,
172 params: normalizePlatformOptions(pubItem.account.type, pubItem.params),
173 }
174 }
175
176 function mergePublishOptions(currentOption: IPlatOption, patchOption: IPlatOption) {
177 const mergedOption = lodash.mergeWith(
178 {},
179 currentOption,
180 patchOption,
181 (_targetValue, patchValue) => {
182 if (Array.isArray(patchValue))
183 return [...patchValue]
184
185 return undefined
186 },
187 ) as IPlatOption
188
189 removeUndefinedOptionFields(mergedOption, patchOption)
190
191 return mergedOption
192 }
193
194 function setPubItemIdentityMap(target: Map<string, PubItem>, pubItem: PubItem) {
195 getSocialAccountIdentityKeys(pubItem.account).forEach((key) => {
196 if (!target.has(key)) {
197 target.set(key, pubItem)
198 }
199 })
200 }
201
202 function getPubItemByAccount(target: Map<string, PubItem>, account: SocialAccount) {
203 for (const key of getSocialAccountIdentityKeys(account)) {
204 const pubItem = target.get(key)
205 if (pubItem)
206 return pubItem
207 }
208
209 return undefined
210 }
211
212 function getDefaultChosenPubItems(pubList: PubItem[], defaultAccountIds?: string[]) {
213 const defaultAccountIdSet = defaultAccountIds && defaultAccountIds.length > 0
214 ? new Set(defaultAccountIds)
215 : undefined
216
217 return pubList.filter(pubItem => (
218 (!defaultAccountIdSet || defaultAccountIdSet.has(pubItem.account.id))
219 && pubItem.account.status !== AccountStatus.DISABLE
220 && isPlatformEnabledSync(pubItem.account.type)
221 ))
222 }
223
224 export const usePublishDialog = create(
225 combine(
226 {
227 ...getStore(),
228 },
229 (set, get, storeApi) => {
230 const methods = {
231 setPrefillLoading(prefillLoading: boolean) {
232 set({ prefillLoading })
233 },
234 setPublishContentLoading(publishContentLoading: boolean) {
235 set({ publishContentLoading })
236 },
237 setPubListChoosed(pubListChoosed: PubItem[]) {
238 set({
239 pubListChoosed: pubListChoosed.map(normalizePubItem),
240 })
241 },
242 setExpandedPubItem(expandedPubItem: PubItem | undefined) {
243 if (!expandedPubItem) {
244 usePublishDialogStorageStore.getState().setExpandedPubItem(undefined)
245 set({ expandedPubItem: undefined })
246 return
247 }
248 const normalizedExpandedPubItem = normalizePubItem(expandedPubItem)
249 usePublishDialogStorageStore.getState().setExpandedPubItem(normalizedExpandedPubItem)
250 set({
251 expandedPubItem: normalizedExpandedPubItem,
252 })
253 },
254 setErrParamsMap(errParamsMap: ErrPubParamsMapType) {
255 set({
256 errParamsMap,
257 })
258 },
259 setWarningParamsMap(warningParamsMap: ErrPubParamsMapType) {
260 set({
261 warningParamsMap,
262 })
263 },
264 setStep(step: number) {
265 set({ step })
266 },
267 setPubTime(pubTime: string | undefined) {
268 set({ pubTime })
269 },
270 setPubList(pubList: PubItem[]) {
271 set({ pubList: pubList.map(normalizePubItem) })
272 },
273
274 // 清空所有数据
275 clear() {
276 set({
277 ...getStore(),
278 })
279 },
280
281 // 初始化发布参数
282 pubParamsInit(accountType?: SocialAccount['type']): IPubParams {
283 const params = lodash.cloneDeep(get().commonPubParams)
284 return accountType ? normalizePlatformOptions(accountType, params) : params
285 },
286
287 /**
288 * 初始化
289 * @param account 账户列表
290 * @param defaultAccountIds 默认选中的账户Id列表
291 */
292 init(account: SocialAccount[], defaultAccountIds?: string[]) {
293 const pubList: PubItem[] = []
294 let hasSelected = false
295
296 // 国外版过滤隐藏平台的账号
297 const filteredAccounts = getVisiblePublishAccounts(account)
298
299 filteredAccounts.map((v) => {
300 pubList.push({
301 account: v,
302 params: methods.pubParamsInit(v.type),
303 })
304 })
305
306 const chosen = getDefaultChosenPubItems(pubList, defaultAccountIds)
307 if (chosen.length > 0) {
308 methods.setPubListChoosed(chosen)
309 hasSelected = chosen.length > 0
310
311 // 如果只有一个,设置为 expandedPubItem
312 if (chosen.length === 1) {
313 set({ expandedPubItem: chosen[0] })
314 }
315 }
316
317 set({
318 pubList,
319 })
320
321 return hasSelected
322 },
323
324 // 同步外部账号刷新结果,保留已编辑的发布参数
325 syncAccounts(account: SocialAccount[], defaultAccountIds?: string[], options?: SyncAccountsOptions) {
326 const filteredAccounts = dedupePublishAccounts(getVisiblePublishAccounts(account))
327 const currentPubList = get().pubList
328 if (filteredAccounts.length === 0 && currentPubList.length > 0) {
329 return get().pubListChoosed.length > 0
330 }
331
332 const currentPubListChoosed = get().pubListChoosed
333 const currentExpandedPubItem = get().expandedPubItem
334 const currentPubItemMap = new Map<string, PubItem>()
335 currentPubList.forEach((pubItem) => {
336 setPubItemIdentityMap(currentPubItemMap, pubItem)
337 })
338 currentPubListChoosed.forEach((pubItem) => {
339 setPubItemIdentityMap(currentPubItemMap, pubItem)
340 })
341 if (currentExpandedPubItem && !currentPubItemMap.has(currentExpandedPubItem.account.id)) {
342 setPubItemIdentityMap(currentPubItemMap, currentExpandedPubItem)
343 }
344
345 const nextPubList = filteredAccounts.map((accountItem) => {
346 const currentPubItem = getPubItemByAccount(currentPubItemMap, accountItem)
347 if (!currentPubItem) {
348 return {
349 account: accountItem,
350 params: methods.pubParamsInit(accountItem.type),
351 }
352 }
353
354 return {
355 ...currentPubItem,
356 account: accountItem,
357 params: normalizePlatformOptions(accountItem.type, currentPubItem.params),
358 }
359 })
360 const nextPubItemMap = new Map<string, PubItem>()
361 nextPubList.forEach((pubItem) => {
362 setPubItemIdentityMap(nextPubItemMap, pubItem)
363 })
364 let nextPubListChoosed = currentPubListChoosed
365 .map(pubItem => getPubItemByAccount(nextPubItemMap, pubItem.account))
366 .filter((pubItem): pubItem is PubItem => Boolean(pubItem))
367 if (options?.applyDefaultWhenEmpty && nextPubListChoosed.length === 0) {
368 nextPubListChoosed = getDefaultChosenPubItems(nextPubList, defaultAccountIds)
369 }
370
371 const nextExpandedPubItem = nextPubListChoosed.length === 1
372 ? nextPubListChoosed[0]
373 : currentExpandedPubItem
374 ? getPubItemByAccount(nextPubItemMap, currentExpandedPubItem.account)
375 : undefined
376
377 set({
378 pubList: nextPubList,
379 pubListChoosed: nextPubListChoosed,
380 expandedPubItem: nextExpandedPubItem,
381 })
382
383 return nextPubListChoosed.length > 0
384 },
385
386 // 参数设置到所有账户
387 setAccountAllParams(pubParmas: Partial<IPubParams>) {
388 const pubList = [...get().pubList]
389 const commonPubParams = { ...get().commonPubParams }
390 let pubListChoosed = [...get().pubListChoosed]
391
392 // 更新 commonPubParams
393 Object.assign(commonPubParams, pubParmas)
394
395 // 更新所有账户的参数(创建新对象引用,避免 memo 缓存导致 UI 不更新)
396 for (let i = 0; i < pubList.length; i++) {
397 const v = pubList[i]
398 const platConfig = getPlatformInfoSync(v.account.type)
399 if (!platConfig)
400 continue
401 const newParams = { ...v.params }
402 let needUpdate = false
403
404 // 更新描述
405 if (pubParmas.des !== undefined) {
406 newParams.des = pubParmas.des
407 needUpdate = true
408 }
409
410 // 更新标题
411 if (pubParmas.title !== undefined && isPublishTitleSupported(v.account.type)) {
412 newParams.title = pubParmas.title
413 needUpdate = true
414 }
415
416 // 更新视频(如果平台支持),使用 Object.hasOwn 以支持传入 undefined 清除视频
417 if (Object.hasOwn(pubParmas, 'video') && platConfig.pubTypes.has(PubType.VIDEO)) {
418 newParams.video = pubParmas.video
419 needUpdate = true
420 }
421
422 // 更新图片(如果平台支持),使用 Object.hasOwn 以支持传入 undefined 清除图片
423 if (Object.hasOwn(pubParmas, 'images') && platConfig.pubTypes.has(PubType.ImageText)) {
424 newParams.images = pubParmas.images
425 needUpdate = true
426 }
427
428 // 更新选项
429 if (pubParmas.option) {
430 newParams.option = mergePublishOptions(v.params.option, pubParmas.option)
431 needUpdate = true
432 }
433
434 // 更新话题
435 if (pubParmas.topics !== undefined) {
436 newParams.topics = pubParmas.topics
437 needUpdate = true
438 }
439
440 if (needUpdate) {
441 const normalizedParams = normalizePlatformOptions(v.account.type, newParams)
442 pubList[i] = { ...v, params: normalizedParams }
443 }
444 }
445
446 pubListChoosed = pubListChoosed.map((v) => {
447 const findData = pubList.find(k => k.account.id === v.account.id)
448 if (findData)
449 return findData
450 return v
451 })
452
453 set({
454 pubList,
455 commonPubParams,
456 pubListChoosed,
457 expandedPubItem: get().expandedPubItem
458 ? pubList.find(v => v.account.id === get().expandedPubItem!.account.id)
459 : undefined,
460 })
461 },
462
463 // 设置单个账号的参数(创建新对象引用,避免 memo 缓存导致 UI 不更新)
464 setOnePubParams(pubParmas: Partial<IPubParams>, accountId: string) {
465 const pubList = [...get().pubList]
466 let pubListChoosed = [...get().pubListChoosed]
467 const index = pubList.findIndex(v => v.account.id === accountId)
468 if (index === -1)
469 return
470
471 const oldItem = pubList[index]
472 const newParams = { ...oldItem.params }
473
474 if (pubParmas.option) {
475 newParams.option = mergePublishOptions(oldItem.params.option, pubParmas.option)
476 }
477
478 const { option: _option, ...paramPatch } = pubParmas
479 Object.assign(newParams, paramPatch)
480
481 const normalizedParams = normalizePlatformOptions(oldItem.account.type, newParams)
482
483 // 创建新的 PubItem 引用
484 pubList[index] = { ...oldItem, params: normalizedParams }
485
486 // 同步更新未选中账号的参数
487 for (let i = 0; i < pubList.length; i++) {
488 if (i === index)
489 continue
490 const v = pubList[i]
491 const platConfig = getPlatformInfoSync(v.account.type)
492 if (!platConfig)
493 continue
494 if (!pubListChoosed.some(k => k.account.id === v.account.id)) {
495 const updatedParams = { ...v.params }
496 let needUpdate = false
497
498 if (pubParmas.des !== undefined) {
499 updatedParams.des = pubParmas.des || ''
500 needUpdate = true
501 }
502 if (platConfig.pubTypes.has(PubType.VIDEO) && pubParmas.video) {
503 updatedParams.video = pubParmas.video
504 needUpdate = true
505 }
506 if (platConfig.pubTypes.has(PubType.ImageText) && pubParmas.images) {
507 updatedParams.images = pubParmas.images
508 needUpdate = true
509 }
510
511 if (needUpdate) {
512 const normalizedParamsForUnselected = normalizePlatformOptions(v.account.type, updatedParams)
513 pubList[i] = { ...v, params: normalizedParamsForUnselected }
514 }
515 }
516 }
517
518 pubListChoosed = pubListChoosed.map((v) => {
519 const findData = pubList.find(k => k.account.id === v.account.id)
520 if (findData)
521 return findData
522 return v
523 })
524
525 set({
526 pubList,
527 pubListChoosed,
528 expandedPubItem: get().expandedPubItem
529 ? pubList.find(v => v.account.id === get().expandedPubItem!.account.id)
530 : undefined,
531 })
532 },
533 }
534
535 return methods
536 },
537 ),
538 )
539
539 lines TYPESCRIPT