返回 AiToEarn
index.ts
1 /**
2 * 草稿箱 Tab 栏 Store
3 * 管理 Tab 列表切换、选中计划持久化、更多面板分页加载
4 */
5
6 import type { IPlanTabStoreState } from './types'
7 import type { PromotionPlan } from '@/api/materials/material.types'
8 import { apiGetMaterialGroupList } from '@/api/materials/material.api'
9 import { createPersistStore } from '@/utils/storage/createPersistStore'
10
11 const PAGE_SIZE = 50
12
13 const initialState: IPlanTabStoreState = {
14 tabPlans: [],
15 tabPlansLoading: false,
16 selectedPlanId: null,
17 morePlans: [],
18 morePlansLoading: false,
19 morePlansPagination: {
20 current: 1,
21 pageSize: PAGE_SIZE,
22 total: 0,
23 hasMore: true,
24 },
25 morePanelOpen: false,
26 initialized: false,
27 }
28
29 export const usePlanTabStore = createPersistStore(
30 initialState,
31 (set, get) => {
32 const mergePlanToTop = (list: PromotionPlan[], plan: PromotionPlan) => {
33 return [plan, ...list.filter(item => item.id !== plan.id)]
34 }
35
36 const syncTabs = async (preferredPlanId?: string | null) => {
37 set({ tabPlansLoading: true })
38 try {
39 const res = await apiGetMaterialGroupList(1, PAGE_SIZE)
40 const list = res?.data?.list || []
41 const total = res?.data?.total || 0
42
43 const { selectedPlanId } = get()
44 const validPreferredPlanId = preferredPlanId && list.some(p => p.id === preferredPlanId)
45 ? preferredPlanId
46 : null
47
48 let validSelectedId: string | null = null
49 if (validPreferredPlanId) {
50 validSelectedId = validPreferredPlanId
51 }
52 else if (selectedPlanId && list.some(p => p.id === selectedPlanId)) {
53 validSelectedId = selectedPlanId
54 }
55 else if (list.length > 0) {
56 validSelectedId = list[0].id
57 }
58
59 set({
60 tabPlans: list,
61 morePlans: list,
62 selectedPlanId: validSelectedId,
63 initialized: true,
64 morePlansPagination: {
65 current: 1,
66 pageSize: PAGE_SIZE,
67 total,
68 hasMore: list.length < total,
69 },
70 })
71 }
72 catch (error) {
73 set({ initialized: true })
74 }
75 finally {
76 set({ tabPlansLoading: false })
77 }
78 }
79
80 const methods = {
81 /**
82 * 初始化 Tab 列表
83 * 加载第一页草稿箱,恢复或默认选中
84 */
85 initTabs: async (preferredPlanId?: string | null) => {
86 const { initialized, tabPlansLoading } = get()
87 if (initialized || tabPlansLoading) {
88 return
89 }
90
91 await syncTabs(preferredPlanId)
92 },
93
94 /**
95 * 强制刷新 Tab 列表
96 * 用于跨页面新增计划后重新拉取
97 */
98 refreshTabs: async (preferredPlanId?: string | null) => {
99 await syncTabs(preferredPlanId)
100 },
101
102 /**
103 * 切换选中计划
104 */
105 selectPlan: (planId: string) => {
106 const { morePanelOpen, selectedPlanId } = get()
107 if (selectedPlanId === planId && !morePanelOpen) {
108 return
109 }
110
111 set({ selectedPlanId: planId, morePanelOpen: false })
112 },
113
114 /**
115 * 加载「更多」面板数据(指定页)
116 */
117 fetchMorePlans: async (page: number = 1) => {
118 set({ morePlansLoading: true })
119 try {
120 const res = await apiGetMaterialGroupList(page, PAGE_SIZE)
121 const list = res?.data?.list || []
122 const total = res?.data?.total || 0
123
124 if (page === 1) {
125 set({ morePlans: list })
126 }
127 else {
128 const { morePlans } = get()
129 set({ morePlans: [...morePlans, ...list] })
130 }
131
132 set({
133 morePlansPagination: {
134 current: page,
135 pageSize: PAGE_SIZE,
136 total,
137 hasMore: page * PAGE_SIZE < total,
138 },
139 })
140 }
141 catch {
142 // 静默失败
143 }
144 finally {
145 set({ morePlansLoading: false })
146 }
147 },
148
149 /**
150 * 面板内加载下一页
151 */
152 loadMoreInPanel: async () => {
153 const { morePlansLoading, morePlansPagination } = get()
154 if (morePlansLoading || !morePlansPagination.hasMore)
155 return
156 await methods.fetchMorePlans(morePlansPagination.current + 1)
157 },
158
159 /**
160 * 创建计划后同步
161 * 刷新 tabPlans + 自动选中新计划
162 */
163 onPlanCreated: async (newPlanId?: string, optimisticPlan?: PromotionPlan) => {
164 if (newPlanId && optimisticPlan) {
165 const { tabPlans, morePlans, morePlansPagination } = get()
166 const nextTabPlans = mergePlanToTop(tabPlans, optimisticPlan)
167 const nextMorePlans = mergePlanToTop(morePlans, optimisticPlan)
168 const total = Math.max(morePlansPagination.total + 1, nextTabPlans.length)
169
170 set({
171 tabPlans: nextTabPlans,
172 morePlans: nextMorePlans,
173 selectedPlanId: newPlanId,
174 initialized: true,
175 morePlansPagination: {
176 ...morePlansPagination,
177 current: 1,
178 total,
179 hasMore: nextTabPlans.length < total,
180 },
181 })
182 }
183
184 try {
185 await syncTabs(newPlanId)
186 }
187 catch {
188 // 静默失败
189 }
190 },
191
192 /**
193 * 删除计划后同步
194 * 从列表移除 + 自动切换到第一个
195 */
196 onPlanDeleted: (deletedPlanId: string) => {
197 const { tabPlans, selectedPlanId, morePlans } = get()
198 const newTabPlans = tabPlans.filter(p => p.id !== deletedPlanId)
199 const newMorePlans = morePlans.filter(p => p.id !== deletedPlanId)
200
201 const updates: Partial<IPlanTabStoreState> = {
202 tabPlans: newTabPlans,
203 morePlans: newMorePlans,
204 }
205
206 // 如果删除的是当前选中计划,切换到第一个
207 if (selectedPlanId === deletedPlanId) {
208 updates.selectedPlanId = newTabPlans.length > 0 ? newTabPlans[0].id : null
209 }
210
211 set(updates as IPlanTabStoreState)
212 },
213
214 /**
215 * 更新计划后同步
216 * 就地更新 tabPlans/morePlans 中的名称
217 */
218 onPlanUpdated: (planId: string, data: { name?: string, desc?: string }) => {
219 const { tabPlans, morePlans } = get()
220
221 const updateList = (list: PromotionPlan[]) =>
222 list.map(p => (p.id === planId ? { ...p, ...data } : p))
223
224 set({
225 tabPlans: updateList(tabPlans),
226 morePlans: updateList(morePlans),
227 } as Partial<IPlanTabStoreState> as IPlanTabStoreState)
228 },
229
230 /**
231 * 打开更多面板
232 */
233 openMorePanel: () => {
234 set({ morePanelOpen: true })
235 // 打开时加载第一页
236 methods.fetchMorePlans(1)
237 },
238
239 /**
240 * 关闭更多面板
241 */
242 closeMorePanel: () => {
243 set({ morePanelOpen: false })
244 },
245
246 /**
247 * 重置 Store
248 */
249 reset: () => {
250 set({
251 ...initialState,
252 selectedPlanId: null,
253 } as IPlanTabStoreState)
254 },
255 }
256
257 return methods
258 },
259 {
260 name: 'plan-tab-store',
261 partialize: (state) => {
262 return { selectedPlanId: state.selectedPlanId } as typeof state
263 },
264 },
265 )
266
266 lines TYPESCRIPT