| import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; |
| import type { ArtworkData, Comment } from './types'; |
| import type { RootState } from '../../store/store'; |
| import { getArtworkById, getCommentsForArtwork } from './mockData'; |
| |
| // ==================== 类型定义 ==================== |
| interface WorkState { |
| currentArtwork: ArtworkData | null; |
| loading: { |
| artwork: boolean; |
| comments: boolean; |
| addComment: boolean; |
| updateArtwork: boolean; |
| deleteComment: boolean; |
| }; |
| error: { |
| artwork: string | null; |
| comments: string | null; |
| addComment: string | null; |
| updateArtwork: string | null; |
| deleteComment: string | null; |
| }; |
| comments: { |
| list: Comment[]; |
| total: number; |
| current: number; |
| pageSize: number; |
| }; |
| } |
| |
| interface FetchCommentsParams { |
| workId: string; |
| page: number; |
| pageSize: number; |
| } |
| |
| interface AddCommentParams { |
| workId: string; |
| content: string; |
| parentId?: string; |
| } |
| |
| interface UpdateArtworkParams { |
| workId: string; |
| updates: Partial<ArtworkData>; |
| } |
| |
| interface DeleteCommentParams { |
| workId: string; |
| commentId: string; |
| } |
| |
| interface SetCommentsPageParams { |
| current: number; |
| pageSize: number; |
| } |
| |
| // ==================== 初始状态 ==================== |
| const initialState: WorkState = { |
| currentArtwork: null, |
| loading: { |
| artwork: false, |
| comments: false, |
| addComment: false, |
| updateArtwork: false, |
| deleteComment: false, |
| }, |
| error: { |
| artwork: null, |
| comments: null, |
| addComment: null, |
| updateArtwork: null, |
| deleteComment: null, |
| }, |
| comments: { |
| list: [], |
| total: 0, |
| current: 1, |
| pageSize: 5, |
| }, |
| }; |
| |
| // ==================== Mock 工具函数 ==================== |
| |
| // 模拟网络延迟 |
| const mockDelay = (ms: number = 800): Promise<void> => |
| new Promise(resolve => setTimeout(resolve, ms)); |
| |
| // 生成新评论ID |
| const generateCommentId = (): string => { |
| return `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
| }; |
| |
| // 生成新评论数据 |
| const createNewComment = (content: string): Comment => { |
| return { |
| id: generateCommentId(), |
| content, |
| author: '当前用户', // 实际应用中从用户状态获取 |
| authorId: 'current_user_id', |
| createdAt: new Date().toLocaleString('zh-CN'), |
| child: [], |
| }; |
| }; |
| |
| // 递归删除评论 |
| const removeCommentById = (comments: Comment[], targetId: string): Comment[] => { |
| return comments.filter(comment => { |
| if (comment.id === targetId) { |
| return false; |
| } |
| if (comment.child.length > 0) { |
| comment.child = removeCommentById(comment.child, targetId); |
| } |
| return true; |
| }); |
| }; |
| |
| // 递归添加回复评论 |
| const addReplyToComment = (comments: Comment[], parentId: string, newComment: Comment): Comment[] => { |
| return comments.map(comment => { |
| if (comment.id === parentId) { |
| return { |
| ...comment, |
| child: [...comment.child, newComment] |
| }; |
| } |
| if (comment.child.length > 0) { |
| return { |
| ...comment, |
| child: addReplyToComment(comment.child, parentId, newComment) |
| }; |
| } |
| return comment; |
| }); |
| }; |
| |
| // 分页处理评论 |
| const paginateComments = (comments: Comment[], page: number, pageSize: number): Comment[] => { |
| const startIndex = (page - 1) * pageSize; |
| const endIndex = startIndex + pageSize; |
| return comments.slice(startIndex, endIndex); |
| }; |
| |
| // ==================== 异步 Actions ==================== |
| |
| // 获取作品详情 |
| export const fetchArtworkDetail = createAsyncThunk< |
| ArtworkData, |
| string, |
| { rejectValue: string } |
| >( |
| 'work/fetchArtworkDetail', |
| async (workId: string, { rejectWithValue }) => { |
| try { |
| await mockDelay(600); // 模拟网络延迟 |
| |
| const artwork = getArtworkById(workId); |
| |
| if (!artwork) { |
| throw new Error(`作品 ${workId} 不存在`); |
| } |
| |
| return artwork; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : '获取作品详情失败'; |
| return rejectWithValue(message); |
| } |
| } |
| ); |
| |
| // 获取评论列表 |
| export const fetchComments = createAsyncThunk< |
| { comments: Comment[]; total: number }, |
| FetchCommentsParams, |
| { rejectValue: string } |
| >( |
| 'work/fetchComments', |
| async ({ workId, page, pageSize }, { rejectWithValue }) => { |
| try { |
| await mockDelay(400); // 模拟网络延迟 |
| |
| const allComments = getCommentsForArtwork(workId); |
| const paginatedComments = paginateComments(allComments, page, pageSize); |
| |
| return { |
| comments: paginatedComments, |
| total: allComments.length |
| }; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : '获取评论失败'; |
| return rejectWithValue(message); |
| } |
| } |
| ); |
| |
| // 添加评论 |
| export const addComment = createAsyncThunk< |
| Comment, |
| AddCommentParams, |
| { rejectValue: string } |
| >( |
| 'work/addComment', |
| async ({ workId, content }, { rejectWithValue }) => { |
| try { |
| await mockDelay(500); // 模拟网络延迟 |
| |
| // 验证作品是否存在 |
| const artwork = getArtworkById(workId); |
| if (!artwork) { |
| throw new Error('作品不存在'); |
| } |
| |
| // 创建新评论 |
| const newComment = createNewComment(content); |
| |
| // 模拟服务器返回完整的评论数据 |
| return newComment; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : '添加评论失败'; |
| return rejectWithValue(message); |
| } |
| } |
| ); |
| |
| // 更新作品信息 |
| export const updateArtwork = createAsyncThunk< |
| ArtworkData, |
| UpdateArtworkParams, |
| { rejectValue: string } |
| >( |
| 'work/updateArtwork', |
| async ({ workId, updates }, { rejectWithValue }) => { |
| try { |
| await mockDelay(1000); // 模拟网络延迟 |
| |
| const currentArtwork = getArtworkById(workId); |
| if (!currentArtwork) { |
| throw new Error('作品不存在'); |
| } |
| |
| // 模拟文件上传处理 |
| const processedUpdates = { ...updates }; |
| |
| // 如果包含 blob URL,模拟转换为正式URL |
| if (updates.artworkCover && updates.artworkCover.startsWith('blob:')) { |
| // 模拟上传成功,生成新的图片URL |
| processedUpdates.artworkCover = `https://picsum.photos/300/400?random=${Date.now()}`; |
| } |
| |
| // 处理版本文件上传 |
| if (updates.versionList) { |
| processedUpdates.versionList = updates.versionList.map(version => ({ |
| ...version, |
| seedFile: version.seedFile.startsWith?.('blob:') |
| ? `magnet:?xt=urn:btih:updated_${Date.now()}&dn=${version.version}.zip` |
| : version.seedFile |
| })); |
| } |
| |
| // 合并更新后的数据 |
| const updatedArtwork: ArtworkData = { |
| ...currentArtwork, |
| ...processedUpdates, |
| updatedAt: new Date().toISOString(), |
| }; |
| |
| return updatedArtwork; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : '更新作品失败'; |
| return rejectWithValue(message); |
| } |
| } |
| ); |
| |
| // 删除评论 |
| export const deleteComment = createAsyncThunk< |
| string, |
| DeleteCommentParams, |
| { rejectValue: string } |
| >( |
| 'work/deleteComment', |
| async ({ workId, commentId }, { rejectWithValue }) => { |
| try { |
| await mockDelay(300); // 模拟网络延迟 |
| |
| // 验证作品是否存在 |
| const artwork = getArtworkById(workId); |
| if (!artwork) { |
| throw new Error('作品不存在'); |
| } |
| |
| // 模拟删除成功 |
| return commentId; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : '删除评论失败'; |
| return rejectWithValue(message); |
| } |
| } |
| ); |
| |
| // ==================== Slice 定义 ==================== |
| const workSlice = createSlice({ |
| name: 'work', |
| initialState, |
| reducers: { |
| // 清除当前作品 |
| clearCurrentArtwork: (state) => { |
| state.currentArtwork = null; |
| state.comments.list = []; |
| state.comments.total = 0; |
| state.comments.current = 1; |
| // 清除所有错误状态 |
| Object.keys(state.error).forEach(key => { |
| state.error[key as keyof typeof state.error] = null; |
| }); |
| }, |
| |
| // 设置评论分页 |
| setCommentsPage: (state, action: PayloadAction<SetCommentsPageParams>) => { |
| state.comments.current = action.payload.current; |
| state.comments.pageSize = action.payload.pageSize; |
| }, |
| |
| // 清除特定错误 |
| clearError: (state, action: PayloadAction<keyof WorkState['error']>) => { |
| state.error[action.payload] = null; |
| }, |
| |
| // 清除所有错误 |
| clearAllErrors: (state) => { |
| Object.keys(state.error).forEach(key => { |
| state.error[key as keyof typeof state.error] = null; |
| }); |
| }, |
| }, |
| extraReducers: (builder) => { |
| // 获取作品详情 |
| builder |
| .addCase(fetchArtworkDetail.pending, (state) => { |
| state.loading.artwork = true; |
| state.error.artwork = null; |
| }) |
| .addCase(fetchArtworkDetail.fulfilled, (state, action) => { |
| state.loading.artwork = false; |
| state.currentArtwork = action.payload; |
| state.error.artwork = null; |
| }) |
| .addCase(fetchArtworkDetail.rejected, (state, action) => { |
| state.loading.artwork = false; |
| state.error.artwork = action.payload || '获取作品详情失败'; |
| }); |
| |
| // 获取评论列表 |
| builder |
| .addCase(fetchComments.pending, (state) => { |
| state.loading.comments = true; |
| state.error.comments = null; |
| }) |
| .addCase(fetchComments.fulfilled, (state, action) => { |
| state.loading.comments = false; |
| state.comments.list = action.payload.comments; |
| state.comments.total = action.payload.total; |
| state.error.comments = null; |
| }) |
| .addCase(fetchComments.rejected, (state, action) => { |
| state.loading.comments = false; |
| state.error.comments = action.payload || '获取评论失败'; |
| }); |
| |
| // 添加评论 |
| builder |
| .addCase(addComment.pending, (state) => { |
| state.loading.addComment = true; |
| state.error.addComment = null; |
| }) |
| .addCase(addComment.fulfilled, (state, action) => { |
| state.loading.addComment = false; |
| |
| const newComment = action.payload; |
| const { parentId } = action.meta.arg; |
| |
| if (parentId) { |
| // 添加回复评论 |
| state.comments.list = addReplyToComment(state.comments.list, parentId, newComment); |
| } else { |
| // 添加顶级评论 |
| state.comments.list.unshift(newComment); |
| state.comments.total += 1; |
| } |
| |
| state.error.addComment = null; |
| }) |
| .addCase(addComment.rejected, (state, action) => { |
| state.loading.addComment = false; |
| state.error.addComment = action.payload || '添加评论失败'; |
| }); |
| |
| // 更新作品信息 |
| builder |
| .addCase(updateArtwork.pending, (state) => { |
| state.loading.updateArtwork = true; |
| state.error.updateArtwork = null; |
| }) |
| .addCase(updateArtwork.fulfilled, (state, action) => { |
| state.loading.updateArtwork = false; |
| state.currentArtwork = action.payload; |
| state.error.updateArtwork = null; |
| }) |
| .addCase(updateArtwork.rejected, (state, action) => { |
| state.loading.updateArtwork = false; |
| state.error.updateArtwork = action.payload || '更新作品失败'; |
| }); |
| |
| // 删除评论 |
| builder |
| .addCase(deleteComment.pending, (state) => { |
| state.loading.deleteComment = true; |
| state.error.deleteComment = null; |
| }) |
| .addCase(deleteComment.fulfilled, (state, action) => { |
| state.loading.deleteComment = false; |
| |
| // 从评论列表中移除已删除的评论 |
| state.comments.list = removeCommentById(state.comments.list, action.payload); |
| state.comments.total = Math.max(0, state.comments.total - 1); |
| state.error.deleteComment = null; |
| }) |
| .addCase(deleteComment.rejected, (state, action) => { |
| state.loading.deleteComment = false; |
| state.error.deleteComment = action.payload || '删除评论失败'; |
| }); |
| }, |
| }); |
| |
| // ==================== Actions 导出 ==================== |
| export const { |
| clearCurrentArtwork, |
| setCommentsPage, |
| clearError, |
| clearAllErrors |
| } = workSlice.actions; |
| |
| // ==================== Selectors 导出 ==================== |
| export const selectCurrentArtwork = (state: RootState): ArtworkData | null => |
| state.work.currentArtwork; |
| |
| export const selectWorkLoading = (state: RootState): WorkState['loading'] => |
| state.work.loading; |
| |
| export const selectWorkError = (state: RootState): WorkState['error'] => |
| state.work.error; |
| |
| export const selectComments = (state: RootState): WorkState['comments'] => |
| state.work.comments; |
| |
| export const selectIsAuthor = (state: RootState): boolean => { |
| const currentUser = state.user; |
| const currentArtwork = state.work.currentArtwork; |
| |
| return Boolean( |
| currentUser?.userid && |
| currentArtwork?.authorId && |
| String(currentUser.userid) === String(currentArtwork.authorId) |
| ); |
| }; |
| |
| // ==================== Reducer 导出 ==================== |
| export default workSlice.reducer; |