| 1 | import type FullCalendar from '@fullcalendar/react' |
| 2 | import type { ChannelPublishRecordItem, ChannelPublishRecordListVo } from '@/api/channels/channel.types' |
| 3 | import type { PublishRecordItem } from '@/api/platforms/publish.types' |
| 4 | import lodash from 'lodash' |
| 5 | import { create } from 'zustand' |
| 6 | import { combine } from 'zustand/middleware' |
| 7 | import { getChannelPublishRecordApi, getChannelPublishRecordsApi } from '@/api/channels/channel.api' |
| 8 | import { PublishStatus } from '@/api/platforms/publish.constants' |
| 9 | import { getDays } from '@/app/[lng]/accounts/components/CalendarTiming/calendarTiming.utils' |
| 10 | import { useAccountStore } from '@/store/account' |
| 11 | |
| 12 | const PUBLISH_RECORD_POLLING_INTERVAL = 20_000 |
| 13 | |
| 14 | const pollingPublishStatuses = new Set<PublishStatus>([ |
| 15 | PublishStatus.QUEUED, |
| 16 | PublishStatus.PUB_LOADING, |
| 17 | ]) |
| 18 | |
| 19 | interface CalendarTimingQueryOptions { |
| 20 | status?: number |
| 21 | dateRange?: [Date, Date] |
| 22 | } |
| 23 | |
| 24 | function shouldPollPublishRecord(status: PublishStatus) { |
| 25 | return pollingPublishStatuses.has(status) |
| 26 | } |
| 27 | |
| 28 | export interface ICalendarTimingStore { |
| 29 | // 日历单元格宽度 |
| 30 | calendarCallWidth: number |
| 31 | // 发布记录数据,key=年月日,value=发布记录 |
| 32 | recordMap: Map<string, PublishRecordItem[]> |
| 33 | // 请求发布记录loading |
| 34 | listLoading: boolean |
| 35 | // 当前日历ref |
| 36 | calendarRef?: FullCalendar |
| 37 | // 是否正在轮询 |
| 38 | polling: boolean |
| 39 | // 最近一次列表查询条件 |
| 40 | lastQueryOptions?: CalendarTimingQueryOptions |
| 41 | } |
| 42 | |
| 43 | const store: ICalendarTimingStore = { |
| 44 | calendarCallWidth: 0, |
| 45 | recordMap: new Map(), |
| 46 | listLoading: false, |
| 47 | calendarRef: undefined, |
| 48 | polling: false, |
| 49 | lastQueryOptions: undefined, |
| 50 | } |
| 51 | |
| 52 | function getStore() { |
| 53 | return lodash.cloneDeep(store) |
| 54 | } |
| 55 | |
| 56 | function getPublishRecordList(data?: ChannelPublishRecordListVo) { |
| 57 | if (!data) { |
| 58 | return [] |
| 59 | } |
| 60 | |
| 61 | if (Array.isArray(data)) { |
| 62 | return data |
| 63 | } |
| 64 | |
| 65 | if (Array.isArray(data.records)) { |
| 66 | return data.records |
| 67 | } |
| 68 | |
| 69 | if (Array.isArray(data.list)) { |
| 70 | return data.list |
| 71 | } |
| 72 | |
| 73 | if (Array.isArray(data.items)) { |
| 74 | return data.items |
| 75 | } |
| 76 | |
| 77 | if (Array.isArray(data.rows)) { |
| 78 | return data.rows |
| 79 | } |
| 80 | |
| 81 | return [] |
| 82 | } |
| 83 | |
| 84 | function normalizePublishRecord(record: ChannelPublishRecordItem): PublishRecordItem { |
| 85 | return { |
| 86 | option: record.option || {}, |
| 87 | userId: record.userId || '', |
| 88 | flowId: record.flowId || '', |
| 89 | userTaskId: record.userTaskId || '', |
| 90 | taskId: record.taskId || record.id, |
| 91 | taskMaterialId: record.taskMaterialId || '', |
| 92 | type: record.type, |
| 93 | title: record.title || '', |
| 94 | desc: record.desc || '', |
| 95 | accountId: record.accountId || '', |
| 96 | topics: record.topics || [], |
| 97 | accountType: record.accountType, |
| 98 | uid: record.uid || '', |
| 99 | videoUrl: record.videoUrl || '', |
| 100 | coverUrl: record.coverUrl || '', |
| 101 | imgUrlList: record.imgUrlList || [], |
| 102 | publishTime: new Date(record.publishTime), |
| 103 | status: Number(record.status) as PublishStatus, |
| 104 | inQueue: record.inQueue || false, |
| 105 | dataId: record.dataId || record.platformWorkId || '', |
| 106 | workLink: record.workLink || '', |
| 107 | linkStatus: record.linkStatus, |
| 108 | linkError: record.linkError, |
| 109 | linkMeta: record.linkMeta, |
| 110 | platformWorkId: record.platformWorkId, |
| 111 | createdAt: record.createdAt || '', |
| 112 | updatedAt: record.updatedAt || '', |
| 113 | id: record.id, |
| 114 | errorMsg: record.errorMsg || '', |
| 115 | engagement: record.engagement, |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | function updateRecordMapItem( |
| 120 | recordMap: Map<string, PublishRecordItem[]>, |
| 121 | nextRecord: PublishRecordItem, |
| 122 | ) { |
| 123 | const nextRecordMap = new Map(recordMap) |
| 124 | let previousRecord: PublishRecordItem | undefined |
| 125 | |
| 126 | nextRecordMap.forEach((recordList, dayKey) => { |
| 127 | const currentRecord = recordList.find(item => item.id === nextRecord.id) |
| 128 | if (currentRecord) { |
| 129 | previousRecord = currentRecord |
| 130 | } |
| 131 | |
| 132 | nextRecordMap.set(dayKey, recordList.filter(item => item.id !== nextRecord.id)) |
| 133 | }) |
| 134 | |
| 135 | const publishTime = getDays(nextRecord.publishTime) |
| 136 | const timeStr = publishTime.format('YYYY-MM-DD') |
| 137 | const nextList = nextRecordMap.get(timeStr) ?? [] |
| 138 | nextList.push(nextRecord) |
| 139 | nextList.sort((a, b) => new Date(a.publishTime).getTime() - new Date(b.publishTime).getTime()) |
| 140 | nextRecordMap.set(timeStr, nextList) |
| 141 | |
| 142 | return previousRecord ? nextRecordMap : recordMap |
| 143 | } |
| 144 | |
| 145 | function removeRecordMapItem(recordMap: Map<string, PublishRecordItem[]>, recordId: string) { |
| 146 | const nextRecordMap = new Map(recordMap) |
| 147 | let hasRemoved = false |
| 148 | |
| 149 | nextRecordMap.forEach((recordList, dayKey) => { |
| 150 | const nextList = recordList.filter(item => item.id !== recordId) |
| 151 | if (nextList.length !== recordList.length) { |
| 152 | hasRemoved = true |
| 153 | nextRecordMap.set(dayKey, nextList) |
| 154 | } |
| 155 | }) |
| 156 | |
| 157 | return hasRemoved ? nextRecordMap : recordMap |
| 158 | } |
| 159 | |
| 160 | export const useCalendarTiming = create( |
| 161 | combine( |
| 162 | { |
| 163 | ...getStore(), |
| 164 | }, |
| 165 | (set, get, storeApi) => { |
| 166 | const methods = { |
| 167 | setCalendarCallWidth(calendarCallWidth: number) { |
| 168 | set({ calendarCallWidth }) |
| 169 | }, |
| 170 | setRecordMap(recordMap: Map<string, PublishRecordItem[]>) { |
| 171 | set({ recordMap }) |
| 172 | }, |
| 173 | setListLoading(listLoading: boolean) { |
| 174 | set({ listLoading }) |
| 175 | }, |
| 176 | setCalendarRef(calendarRef: FullCalendar) { |
| 177 | set({ calendarRef }) |
| 178 | }, |
| 179 | setPolling(polling: boolean) { |
| 180 | set({ polling }) |
| 181 | }, |
| 182 | removePubRecord(recordId: string) { |
| 183 | const recordMap = removeRecordMapItem(get().recordMap, recordId) |
| 184 | methods.setRecordMap(recordMap) |
| 185 | }, |
| 186 | async refreshPubRecordDetail(recordId: string) { |
| 187 | const res = await getChannelPublishRecordApi(recordId) |
| 188 | if (!res || !res.data) { |
| 189 | return undefined |
| 190 | } |
| 191 | |
| 192 | const newPubRecord = normalizePublishRecord(res.data) |
| 193 | const recordMap = updateRecordMapItem(get().recordMap, newPubRecord) |
| 194 | methods.setRecordMap(recordMap) |
| 195 | |
| 196 | if (shouldPollPublishRecord(newPubRecord.status)) { |
| 197 | methods.queryPubTask() |
| 198 | } |
| 199 | |
| 200 | return newPubRecord |
| 201 | }, |
| 202 | |
| 203 | // 获取发布记录数据 |
| 204 | // dateRange: 可选的日期范围参数,用于周视图场景 |
| 205 | async getPubRecord(options?: CalendarTimingQueryOptions) { |
| 206 | const { status, dateRange } = options || {} |
| 207 | set({ lastQueryOptions: options }) |
| 208 | |
| 209 | try { |
| 210 | methods.setListLoading(true) |
| 211 | |
| 212 | let startDay, endDay |
| 213 | |
| 214 | if (dateRange) { |
| 215 | // 使用传入的日期范围(周视图场景) |
| 216 | startDay = getDays(dateRange[0]).startOf('day') |
| 217 | endDay = getDays(dateRange[1]).endOf('day') |
| 218 | } |
| 219 | else { |
| 220 | // 使用 FullCalendar 的日期范围(月视图场景) |
| 221 | const calendarApi = get().calendarRef?.getApi() |
| 222 | const view = calendarApi?.view |
| 223 | const visibleStart = view?.activeStart |
| 224 | const visibleEnd = view?.activeEnd |
| 225 | |
| 226 | if (visibleStart && visibleEnd) { |
| 227 | startDay = getDays(visibleStart).startOf('day') |
| 228 | endDay = getDays(visibleEnd).subtract(1, 'day').endOf('day') |
| 229 | } |
| 230 | else { |
| 231 | // calendarRef 不可用时(移动端、未初始化等),默认当月范围 |
| 232 | startDay = getDays().startOf('month').startOf('day') |
| 233 | endDay = getDays().endOf('month').endOf('day') |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | const activeAccount = useAccountStore.getState().accountActive |
| 238 | const requestParams = { |
| 239 | time: [startDay.toISOString(), endDay.toISOString()] as [string, string], |
| 240 | accountType: activeAccount?.type, |
| 241 | status, |
| 242 | } |
| 243 | |
| 244 | const res = await getChannelPublishRecordsApi(requestParams) |
| 245 | methods.setListLoading(false) |
| 246 | const records = getPublishRecordList(res?.data) |
| 247 | |
| 248 | // 检查响应数据是否有效 |
| 249 | if (!res || !res.data) { |
| 250 | console.warn('获取发布记录数据失败或数据格式不正确:', res) |
| 251 | methods.setRecordMap(new Map()) |
| 252 | return |
| 253 | } |
| 254 | |
| 255 | const recordMap = new Map<string, PublishRecordItem[]>() |
| 256 | // 将数据分拣到对应天中 |
| 257 | records.map((record) => { |
| 258 | const v = normalizePublishRecord(record) |
| 259 | const days = getDays(v.publishTime) |
| 260 | const timeStr = days.format('YYYY-MM-DD') |
| 261 | let list = recordMap.get(timeStr) |
| 262 | if (!list) { |
| 263 | list = [] |
| 264 | recordMap.set(timeStr, list) |
| 265 | } |
| 266 | list.push(v) |
| 267 | recordMap.set(timeStr, list) |
| 268 | }) |
| 269 | // 对每一天的记录按照 publishTime 时间从早到晚排序 |
| 270 | recordMap.forEach((v, k) => { |
| 271 | let list = recordMap.get(k) |
| 272 | if (list) { |
| 273 | list = list.sort( |
| 274 | (a, b) => |
| 275 | new Date(a?.publishTime ?? 0).getTime() |
| 276 | - new Date(b?.publishTime ?? 0).getTime(), |
| 277 | ) |
| 278 | recordMap.set(k, list) |
| 279 | } |
| 280 | }) |
| 281 | methods.setRecordMap(recordMap) |
| 282 | // 获取完数据后,启动轮询检查 |
| 283 | methods.queryPubTask() |
| 284 | } |
| 285 | catch (error) { |
| 286 | console.error('获取发布记录数据时发生错误:', error) |
| 287 | methods.setListLoading(false) |
| 288 | methods.setRecordMap(new Map()) |
| 289 | } |
| 290 | }, |
| 291 | |
| 292 | async refreshCurrentPubRecords() { |
| 293 | await methods.getPubRecord(get().lastQueryOptions) |
| 294 | }, |
| 295 | |
| 296 | // 查询列表是否有在队列中/发布中的数据,如果有则串行轮询查询详情,直到状态完成 |
| 297 | async queryPubTask() { |
| 298 | if (get().polling) { |
| 299 | return |
| 300 | } |
| 301 | methods.setPolling(true) |
| 302 | |
| 303 | let pollRecord: PublishRecordItem | null = null |
| 304 | const recordMap = get().recordMap |
| 305 | |
| 306 | // 遍历 recordMap 查找队列中或发布中的记录,同一时间只轮询一条 |
| 307 | for (const [_dayKey, recordList] of recordMap.entries()) { |
| 308 | if (pollRecord !== null) { |
| 309 | break |
| 310 | } |
| 311 | for (const item of recordList) { |
| 312 | if (shouldPollPublishRecord(item.status)) { |
| 313 | pollRecord = item |
| 314 | break |
| 315 | } |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | if (pollRecord === null) { |
| 320 | methods.setPolling(false) |
| 321 | return |
| 322 | } |
| 323 | |
| 324 | methods._pollQueryPubTask(pollRecord) |
| 325 | }, |
| 326 | |
| 327 | // 轮询查询发布任务详情 |
| 328 | async _pollQueryPubTask(pubRecord: PublishRecordItem) { |
| 329 | await new Promise(resolve => setTimeout(resolve, PUBLISH_RECORD_POLLING_INTERVAL)) |
| 330 | |
| 331 | try { |
| 332 | const res = await getChannelPublishRecordApi(pubRecord.id) |
| 333 | if (!res || !res.data) { |
| 334 | methods.setPolling(false) |
| 335 | return |
| 336 | } |
| 337 | |
| 338 | const newPubRecord = normalizePublishRecord(res.data) |
| 339 | const recordMap = updateRecordMapItem(get().recordMap, newPubRecord) |
| 340 | methods.setRecordMap(recordMap) |
| 341 | |
| 342 | if (shouldPollPublishRecord(newPubRecord.status)) { |
| 343 | // 状态仍为队列中或发布中,继续轮询当前记录 |
| 344 | methods._pollQueryPubTask(newPubRecord) |
| 345 | } |
| 346 | else { |
| 347 | methods.setPolling(false) |
| 348 | // 当前记录已结束,继续串行查询下一条队列中或发布中的任务 |
| 349 | methods.queryPubTask() |
| 350 | } |
| 351 | } |
| 352 | catch (error) { |
| 353 | console.error('轮询查询发布任务详情时发生错误:', error) |
| 354 | methods.setPolling(false) |
| 355 | } |
| 356 | }, |
| 357 | |
| 358 | clear() { |
| 359 | set({ |
| 360 | ...getStore(), |
| 361 | }) |
| 362 | }, |
| 363 | } |
| 364 | return methods |
| 365 | }, |
| 366 | ), |
| 367 | ) |
| 368 |