blob: 6b2c12cfc262a8ae6d759fbd6ba3cf38f5a145b7 [file] [log] [blame]
223010144ce05872025-06-08 22:33:28 +08001import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit';
2import type { ArtworkData, Comment } from './types';
3import type { RootState } from '../../store/store';
4import { getArtworkById, getCommentsForArtwork } from './mockData';
5
6// ==================== 类型定义 ====================
7interface WorkState {
8 currentArtwork: ArtworkData | null;
9 loading: {
10 artwork: boolean;
11 comments: boolean;
12 addComment: boolean;
13 updateArtwork: boolean;
14 deleteComment: boolean;
15 };
16 error: {
17 artwork: string | null;
18 comments: string | null;
19 addComment: string | null;
20 updateArtwork: string | null;
21 deleteComment: string | null;
22 };
23 comments: {
24 list: Comment[];
25 total: number;
26 current: number;
27 pageSize: number;
28 };
29}
30
31interface FetchCommentsParams {
32 workId: string;
33 page: number;
34 pageSize: number;
35}
36
37interface AddCommentParams {
38 workId: string;
39 content: string;
40 parentId?: string;
41}
42
43interface UpdateArtworkParams {
44 workId: string;
45 updates: Partial<ArtworkData>;
46}
47
48interface DeleteCommentParams {
49 workId: string;
50 commentId: string;
51}
52
53interface SetCommentsPageParams {
54 current: number;
55 pageSize: number;
56}
57
58// ==================== 初始状态 ====================
59const initialState: WorkState = {
60 currentArtwork: null,
61 loading: {
62 artwork: false,
63 comments: false,
64 addComment: false,
65 updateArtwork: false,
66 deleteComment: false,
67 },
68 error: {
69 artwork: null,
70 comments: null,
71 addComment: null,
72 updateArtwork: null,
73 deleteComment: null,
74 },
75 comments: {
76 list: [],
77 total: 0,
78 current: 1,
79 pageSize: 5,
80 },
81};
82
83// ==================== Mock 工具函数 ====================
84
85// 模拟网络延迟
86const mockDelay = (ms: number = 800): Promise<void> =>
87 new Promise(resolve => setTimeout(resolve, ms));
88
89// 生成新评论ID
90const generateCommentId = (): string => {
91 return `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
92};
93
94// 生成新评论数据
95const createNewComment = (content: string): Comment => {
96 return {
97 id: generateCommentId(),
98 content,
99 author: '当前用户', // 实际应用中从用户状态获取
100 authorId: 'current_user_id',
101 createdAt: new Date().toLocaleString('zh-CN'),
102 child: [],
103 };
104};
105
106// 递归删除评论
107const removeCommentById = (comments: Comment[], targetId: string): Comment[] => {
108 return comments.filter(comment => {
109 if (comment.id === targetId) {
110 return false;
111 }
112 if (comment.child.length > 0) {
113 comment.child = removeCommentById(comment.child, targetId);
114 }
115 return true;
116 });
117};
118
119// 递归添加回复评论
120const addReplyToComment = (comments: Comment[], parentId: string, newComment: Comment): Comment[] => {
121 return comments.map(comment => {
122 if (comment.id === parentId) {
123 return {
124 ...comment,
125 child: [...comment.child, newComment]
126 };
127 }
128 if (comment.child.length > 0) {
129 return {
130 ...comment,
131 child: addReplyToComment(comment.child, parentId, newComment)
132 };
133 }
134 return comment;
135 });
136};
137
138// 分页处理评论
139const paginateComments = (comments: Comment[], page: number, pageSize: number): Comment[] => {
140 const startIndex = (page - 1) * pageSize;
141 const endIndex = startIndex + pageSize;
142 return comments.slice(startIndex, endIndex);
143};
144
145// ==================== 异步 Actions ====================
146
147// 获取作品详情
148export const fetchArtworkDetail = createAsyncThunk<
149 ArtworkData,
150 string,
151 { rejectValue: string }
152>(
153 'work/fetchArtworkDetail',
154 async (workId: string, { rejectWithValue }) => {
155 try {
156 await mockDelay(600); // 模拟网络延迟
157
158 const artwork = getArtworkById(workId);
159
160 if (!artwork) {
161 throw new Error(`作品 ${workId} 不存在`);
162 }
163
164 return artwork;
165 } catch (error) {
166 const message = error instanceof Error ? error.message : '获取作品详情失败';
167 return rejectWithValue(message);
168 }
169 }
170);
171
172// 获取评论列表
173export const fetchComments = createAsyncThunk<
174 { comments: Comment[]; total: number },
175 FetchCommentsParams,
176 { rejectValue: string }
177>(
178 'work/fetchComments',
179 async ({ workId, page, pageSize }, { rejectWithValue }) => {
180 try {
181 await mockDelay(400); // 模拟网络延迟
182
183 const allComments = getCommentsForArtwork(workId);
184 const paginatedComments = paginateComments(allComments, page, pageSize);
185
186 return {
187 comments: paginatedComments,
188 total: allComments.length
189 };
190 } catch (error) {
191 const message = error instanceof Error ? error.message : '获取评论失败';
192 return rejectWithValue(message);
193 }
194 }
195);
196
197// 添加评论
198export const addComment = createAsyncThunk<
199 Comment,
200 AddCommentParams,
201 { rejectValue: string }
202>(
203 'work/addComment',
204 async ({ workId, content }, { rejectWithValue }) => {
205 try {
206 await mockDelay(500); // 模拟网络延迟
207
208 // 验证作品是否存在
209 const artwork = getArtworkById(workId);
210 if (!artwork) {
211 throw new Error('作品不存在');
212 }
213
214 // 创建新评论
215 const newComment = createNewComment(content);
216
217 // 模拟服务器返回完整的评论数据
218 return newComment;
219 } catch (error) {
220 const message = error instanceof Error ? error.message : '添加评论失败';
221 return rejectWithValue(message);
222 }
223 }
224);
225
226// 更新作品信息
227export const updateArtwork = createAsyncThunk<
228 ArtworkData,
229 UpdateArtworkParams,
230 { rejectValue: string }
231>(
232 'work/updateArtwork',
233 async ({ workId, updates }, { rejectWithValue }) => {
234 try {
235 await mockDelay(1000); // 模拟网络延迟
236
237 const currentArtwork = getArtworkById(workId);
238 if (!currentArtwork) {
239 throw new Error('作品不存在');
240 }
241
242 // 模拟文件上传处理
243 const processedUpdates = { ...updates };
244
245 // 如果包含 blob URL,模拟转换为正式URL
246 if (updates.artworkCover && updates.artworkCover.startsWith('blob:')) {
247 // 模拟上传成功,生成新的图片URL
248 processedUpdates.artworkCover = `https://picsum.photos/300/400?random=${Date.now()}`;
249 }
250
251 // 处理版本文件上传
252 if (updates.versionList) {
253 processedUpdates.versionList = updates.versionList.map(version => ({
254 ...version,
255 seedFile: version.seedFile.startsWith?.('blob:')
256 ? `magnet:?xt=urn:btih:updated_${Date.now()}&dn=${version.version}.zip`
257 : version.seedFile
258 }));
259 }
260
261 // 合并更新后的数据
262 const updatedArtwork: ArtworkData = {
263 ...currentArtwork,
264 ...processedUpdates,
265 updatedAt: new Date().toISOString(),
266 };
267
268 return updatedArtwork;
269 } catch (error) {
270 const message = error instanceof Error ? error.message : '更新作品失败';
271 return rejectWithValue(message);
272 }
273 }
274);
275
276// 删除评论
277export const deleteComment = createAsyncThunk<
278 string,
279 DeleteCommentParams,
280 { rejectValue: string }
281>(
282 'work/deleteComment',
283 async ({ workId, commentId }, { rejectWithValue }) => {
284 try {
285 await mockDelay(300); // 模拟网络延迟
286
287 // 验证作品是否存在
288 const artwork = getArtworkById(workId);
289 if (!artwork) {
290 throw new Error('作品不存在');
291 }
292
293 // 模拟删除成功
294 return commentId;
295 } catch (error) {
296 const message = error instanceof Error ? error.message : '删除评论失败';
297 return rejectWithValue(message);
298 }
299 }
300);
301
302// ==================== Slice 定义 ====================
303const workSlice = createSlice({
304 name: 'work',
305 initialState,
306 reducers: {
307 // 清除当前作品
308 clearCurrentArtwork: (state) => {
309 state.currentArtwork = null;
310 state.comments.list = [];
311 state.comments.total = 0;
312 state.comments.current = 1;
313 // 清除所有错误状态
314 Object.keys(state.error).forEach(key => {
315 state.error[key as keyof typeof state.error] = null;
316 });
317 },
318
319 // 设置评论分页
320 setCommentsPage: (state, action: PayloadAction<SetCommentsPageParams>) => {
321 state.comments.current = action.payload.current;
322 state.comments.pageSize = action.payload.pageSize;
323 },
324
325 // 清除特定错误
326 clearError: (state, action: PayloadAction<keyof WorkState['error']>) => {
327 state.error[action.payload] = null;
328 },
329
330 // 清除所有错误
331 clearAllErrors: (state) => {
332 Object.keys(state.error).forEach(key => {
333 state.error[key as keyof typeof state.error] = null;
334 });
335 },
336 },
337 extraReducers: (builder) => {
338 // 获取作品详情
339 builder
340 .addCase(fetchArtworkDetail.pending, (state) => {
341 state.loading.artwork = true;
342 state.error.artwork = null;
343 })
344 .addCase(fetchArtworkDetail.fulfilled, (state, action) => {
345 state.loading.artwork = false;
346 state.currentArtwork = action.payload;
347 state.error.artwork = null;
348 })
349 .addCase(fetchArtworkDetail.rejected, (state, action) => {
350 state.loading.artwork = false;
351 state.error.artwork = action.payload || '获取作品详情失败';
352 });
353
354 // 获取评论列表
355 builder
356 .addCase(fetchComments.pending, (state) => {
357 state.loading.comments = true;
358 state.error.comments = null;
359 })
360 .addCase(fetchComments.fulfilled, (state, action) => {
361 state.loading.comments = false;
362 state.comments.list = action.payload.comments;
363 state.comments.total = action.payload.total;
364 state.error.comments = null;
365 })
366 .addCase(fetchComments.rejected, (state, action) => {
367 state.loading.comments = false;
368 state.error.comments = action.payload || '获取评论失败';
369 });
370
371 // 添加评论
372 builder
373 .addCase(addComment.pending, (state) => {
374 state.loading.addComment = true;
375 state.error.addComment = null;
376 })
377 .addCase(addComment.fulfilled, (state, action) => {
378 state.loading.addComment = false;
379
380 const newComment = action.payload;
381 const { parentId } = action.meta.arg;
382
383 if (parentId) {
384 // 添加回复评论
385 state.comments.list = addReplyToComment(state.comments.list, parentId, newComment);
386 } else {
387 // 添加顶级评论
388 state.comments.list.unshift(newComment);
389 state.comments.total += 1;
390 }
391
392 state.error.addComment = null;
393 })
394 .addCase(addComment.rejected, (state, action) => {
395 state.loading.addComment = false;
396 state.error.addComment = action.payload || '添加评论失败';
397 });
398
399 // 更新作品信息
400 builder
401 .addCase(updateArtwork.pending, (state) => {
402 state.loading.updateArtwork = true;
403 state.error.updateArtwork = null;
404 })
405 .addCase(updateArtwork.fulfilled, (state, action) => {
406 state.loading.updateArtwork = false;
407 state.currentArtwork = action.payload;
408 state.error.updateArtwork = null;
409 })
410 .addCase(updateArtwork.rejected, (state, action) => {
411 state.loading.updateArtwork = false;
412 state.error.updateArtwork = action.payload || '更新作品失败';
413 });
414
415 // 删除评论
416 builder
417 .addCase(deleteComment.pending, (state) => {
418 state.loading.deleteComment = true;
419 state.error.deleteComment = null;
420 })
421 .addCase(deleteComment.fulfilled, (state, action) => {
422 state.loading.deleteComment = false;
423
424 // 从评论列表中移除已删除的评论
425 state.comments.list = removeCommentById(state.comments.list, action.payload);
426 state.comments.total = Math.max(0, state.comments.total - 1);
427 state.error.deleteComment = null;
428 })
429 .addCase(deleteComment.rejected, (state, action) => {
430 state.loading.deleteComment = false;
431 state.error.deleteComment = action.payload || '删除评论失败';
432 });
433 },
434});
435
436// ==================== Actions 导出 ====================
437export const {
438 clearCurrentArtwork,
439 setCommentsPage,
440 clearError,
441 clearAllErrors
442} = workSlice.actions;
443
444// ==================== Selectors 导出 ====================
445export const selectCurrentArtwork = (state: RootState): ArtworkData | null =>
446 state.work.currentArtwork;
447
448export const selectWorkLoading = (state: RootState): WorkState['loading'] =>
449 state.work.loading;
450
451export const selectWorkError = (state: RootState): WorkState['error'] =>
452 state.work.error;
453
454export const selectComments = (state: RootState): WorkState['comments'] =>
455 state.work.comments;
456
457export const selectIsAuthor = (state: RootState): boolean => {
458 const currentUser = state.user;
459 const currentArtwork = state.work.currentArtwork;
460
461 return Boolean(
462 currentUser?.userid &&
463 currentArtwork?.authorId &&
464 String(currentUser.userid) === String(currentArtwork.authorId)
465 );
466};
467
468// ==================== Reducer 导出 ====================
469export default workSlice.reducer;