'use client'; import React, { createContext, useContext, useCallback, useState, useMemo, useEffect } from 'react'; import { useQuery, keepPreviousData, useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter, useSearchParams } from 'next/navigation'; import { useTRPC, usePost } from '@fenghuo/client'; import { Prisma } from '@fenghuo/db'; import type { Article } from '@fenghuo/common'; import { PostStatus, PostType } from '@fenghuo/common/post'; import type { FilterState, PaginationState, ArticleStats, ArticleAction, BatchActionType } from '@/lib/articles/types'; import { DEFAULT_PAGE_SIZE } from '@/lib/articles/constants'; import { type Editor } from '@tiptap/react'; import { toast } from '@nice/ui/components/sonner'; import { useAuth } from '@/components/providers/auth-provider'; // 后端返回的stats类型 interface BackendStats { all: number; published: number; draft: number; pending: number; private: number; trash: number; } // Context 类型定义 interface ArticlesContextValue { // 数据 articles: Article[]; stats: ArticleStats; isLoading: boolean; // 状态 filters: FilterState; pagination: PaginationState; selectedArticles: string[]; quickEditId: string | null; batchAction: string; // 筛选和分页操作 updateFilters: (updates: Partial) => void; setCurrentPage: (page: number) => void; // 选择操作 handleSelectAll: (checked: boolean) => void; handleSelectArticle: (articleId: string, checked: boolean) => void; resetSelection: () => void; setQuickEditId: (id: string | null) => void; setBatchAction: (action: string) => void; // 文章操作 handleQuickAction: (articleId: string, action: string) => void; handleBatchAction: () => void; handleArticleAction: (articleId: string, action: ArticleAction) => void; handleQuickEditSave: (articleId: string, updates: Partial
) => void; updateVisibility: (articleId: string, visibility: 'public' | 'private') => void; refetchArticles: () => void; } const ArticlesContext = createContext(null); // Hook 来使用 Context export function useArticlesContext() { const context = useContext(ArticlesContext); if (!context) { throw new Error('useArticlesContext must be used within ArticlesProvider'); } return context; } // 将后端stats转换为前端期望的格式 function transformStats(backendStats: BackendStats): ArticleStats { return { all: backendStats.all, published: backendStats.published, draft: backendStats.draft, archived: backendStats.private, // 将private映射为archived deleted: backendStats.trash ?? 0, // 将trash映射为deleted totalViews: 0, // TODO: 后续从其他接口获取 totalLikes: 0, // TODO: 后续从其他接口获取 }; } // Provider 组件 export function ArticlesProvider({ children }: { children: React.ReactNode }) { const router = useRouter(); const trpc = useTRPC(); const { update: updateArticle, softDeleteByIds } = usePost(); // 状态管理 const [filters, setFilters] = useState({ searchTerm: '', statusFilter: 'all', categoryFilter: 'all', sortBy: 'created-desc', }); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: DEFAULT_PAGE_SIZE, totalPages: 1, totalCount: 0, }); const [selectedArticles, setSelectedArticles] = useState([]); const [quickEditId, setQuickEditId] = useState(null); const [batchAction, setBatchAction] = useState(''); // 构建查询条件 const { where, orderBy } = useMemo(() => { const where: Prisma.PostWhereInput = {}; const orderBy: Prisma.PostOrderByWithRelationInput = {}; const andConditions: Prisma.PostWhereInput[] = []; if (filters.statusFilter === 'trash') { andConditions.push({ deletedAt: { not: null } } as any); } else { andConditions.push({ deletedAt: null } as any); if (filters.statusFilter !== 'all') { andConditions.push({ status: filters.statusFilter as any }); } } if (filters.searchTerm) { andConditions.push({ OR: [ { title: { contains: filters.searchTerm, mode: 'insensitive' } }, { content: { contains: filters.searchTerm, mode: 'insensitive' } }, ], }); } if (filters.categoryFilter !== 'all') { andConditions.push({ terms: { some: { id: filters.categoryFilter } } }); } if (andConditions.length > 0) { where.AND = andConditions; } // 排序 switch (filters.sortBy) { case 'created-asc': orderBy.createdAt = 'asc'; break; case 'created-desc': orderBy.createdAt = 'desc'; break; case 'published-asc': orderBy.publishedAt = 'asc'; break; case 'published-desc': orderBy.publishedAt = 'desc'; break; case 'title-asc': orderBy.title = 'asc'; break; case 'title-desc': orderBy.title = 'desc'; break; } return { where, orderBy }; }, [filters]); // 数据查询 const { data: articlesData, isLoading: isLoadingArticles, refetch: refetchArticles, } = useQuery( trpc.post.findManyWithPagination.queryOptions( { page: pagination.currentPage, pageSize: pagination.pageSize, where, orderBy, }, { placeholderData: keepPreviousData, }, ), ); const { data: backendStatsData, isLoading: isLoadingStats } = useQuery(trpc.post.getStats.queryOptions({ where })); // 转换后端stats数据 const stats = useMemo(() => { if (!backendStatsData) { return { all: 0, published: 0, draft: 0, archived: 0, deleted: 0, totalViews: 0, totalLikes: 0, }; } return transformStats(backendStatsData as any); }, [backendStatsData]); // 更新分页信息 useEffect(() => { if (articlesData) { setPagination((prev) => ({ ...prev, totalPages: articlesData.totalPages, totalCount: articlesData.totalCount, })); } }, [articlesData]); // 筛选和分页操作 const updateFilters = useCallback((updates: Partial) => { setFilters((prev) => ({ ...prev, ...updates })); setPagination((prev) => ({ ...prev, currentPage: 1 })); }, []); const setCurrentPage = useCallback((page: number) => { setPagination((prev) => ({ ...prev, currentPage: page })); }, []); // 选择操作 const handleSelectAll = useCallback( (checked: boolean) => { if (checked) { setSelectedArticles(articlesData?.items.map((a) => a.id) ?? []); } else { setSelectedArticles([]); } }, [articlesData?.items], ); const handleSelectArticle = useCallback((articleId: string, checked: boolean) => { setSelectedArticles((prev) => (checked ? [...prev, articleId] : prev.filter((id) => id !== articleId))); }, []); const resetSelection = useCallback(() => { setSelectedArticles([]); setBatchAction(''); }, []); const updateVisibility = useCallback( (articleId: string, visibility: 'public' | 'private') => { updateArticle.mutate({ where: { id: articleId }, data: { visibility }, }); }, [updateArticle], ); // 文章操作 const handleQuickAction = useCallback( (articleId: string, action: string) => { switch (action) { case 'publish': updateArticle.mutate({ where: { id: articleId }, data: { status: 'PUBLISHED', publishedAt: new Date() }, }); break; case 'draft': updateArticle.mutate({ where: { id: articleId }, data: { status: 'DRAFT' }, }); break; case 'archive': updateArticle.mutate({ where: { id: articleId }, data: { status: 'ARCHIVED' }, }); break; case 'delete': softDeleteByIds.mutate({ ids: [articleId] }); break; default: console.warn('未知快速操作', action); } }, [updateArticle, softDeleteByIds], ); // 批量操作 const handleBatchAction = useCallback(() => { if (selectedArticles.length === 0 || !batchAction) return; const actionHandlers = { publish: { status: 'PUBLISHED', publishedAt: new Date() }, draft: { status: 'DRAFT' }, archive: { status: 'ARCHIVED' }, }; if (batchAction in actionHandlers) { const data = actionHandlers[batchAction as keyof typeof actionHandlers]; updateArticle.mutate({ where: { id: { in: selectedArticles } } as any, data, }); } else if (batchAction === 'delete') { softDeleteByIds.mutate({ ids: selectedArticles }); } resetSelection(); }, [batchAction, selectedArticles, updateArticle, softDeleteByIds, resetSelection]); const handleArticleAction = useCallback( (articleId: string, action: ArticleAction) => { switch (action) { case 'edit': router.push(`/editor?id=${articleId}`); break; case 'publish': updateArticle.mutate({ where: { id: articleId }, data: { status: 'PUBLISHED', publishedAt: new Date() }, }); break; case 'unpublish': updateArticle.mutate({ where: { id: articleId }, data: { status: 'DRAFT' } }); break; case 'archive': updateArticle.mutate({ where: { id: articleId }, data: { status: 'ARCHIVED' } }); break; case 'delete': softDeleteByIds.mutate({ ids: [articleId] }); break; case 'duplicate': // TODO: 实现复制功能 toast.info('此功能正在开发中'); break; case 'preview': // TODO: 实现预览功能 toast.info('此功能正在开发中'); break; case 'share': // TODO: 实现分享功能 toast.info('此功能正在开发中'); console.log(`分享文章: ${articleId}`); break; } }, [router, updateArticle, softDeleteByIds], ); const handleQuickEditSave = useCallback( (articleId: string, updates: Partial
) => { updateArticle.mutate({ where: { id: articleId }, data: updates as any, }); setQuickEditId(null); }, [updateArticle], ); const contextValue: ArticlesContextValue = { // 数据 articles: articlesData?.items ?? [], stats, isLoading: isLoadingArticles || isLoadingStats, // 状态 filters, pagination, selectedArticles, quickEditId, batchAction, // 筛选和分页操作 updateFilters, setCurrentPage, // 选择操作 handleSelectAll, handleSelectArticle, resetSelection, setQuickEditId, setBatchAction, // 文章操作 handleQuickAction, handleBatchAction, handleArticleAction, handleQuickEditSave, updateVisibility, refetchArticles, }; return {children}; } // 文章编辑器状态类型 interface EditorState { // 文章基本信息 id?: string; title: string; excerpt: string; content: string; status: PostStatus; publishedAt?: Date; organizationId: string; order: number; // 分类信息 terms: { id: string; taxonomyId: string }[]; // 编辑器状态 isLoading: boolean; isSaving: boolean; lastSaved?: Date; hasUnsavedChanges: boolean; } // 上下文值类型 interface EditorContextValue { // 状态 state: EditorState; // TipTap 编辑器实例 editor: Editor | null; // 动作 setEditor: (editor: Editor | null) => void; updateContent: (content: string) => void; updateField: (field: K, value: EditorState[K]) => void; // 保存操作 save: (options?: { newStatus?: PostStatus; successMessage?: string }) => Promise; // 重置状态 reset: () => void; } const defaultState: EditorState = { title: '', excerpt: '', content: '', status: PostStatus.DRAFT, organizationId: '', order: 0, terms: [], isLoading: false, isSaving: false, hasUnsavedChanges: false, }; const EditorContext = createContext(null); export function useEditorContext() { const context = useContext(EditorContext); if (!context) { throw new Error('useEditorContext must be used within EditorProvider'); } return context; } interface EditorProviderProps { children: React.ReactNode; } export function EditorProvider({ children }: EditorProviderProps) { const router = useRouter(); const trpc = useTRPC(); const { create, update } = usePost(); const searchParams = useSearchParams(); const articleId = searchParams.get('id'); // 获取当前用户信息 const { user: currentUser } = useAuth(); const [state, setState] = useState(defaultState); const [editor, setEditor] = useState(null); // 获取文章数据 const { data: postData, isLoading } = useQuery({ ...trpc.post.findFirst.queryOptions({ where: { id: articleId || '' }, select: { id: true, title: true, content: true, excerpt: true, status: true, publishedAt: true, organizationId: true, order: true, terms: { select: { id: true, taxonomyId: true } }, }, }), enabled: !!articleId, }); // 获取分类数据用于验证 const { data: taxonomiesData } = useQuery({ ...trpc.taxonomy.findMany.queryOptions({ where: { postTypes: { has: PostType.ARTICLE, }, }, }), }); // 初始化文章数据 useEffect(() => { if (postData) { setState((prev) => ({ ...prev, id: postData.id, title: postData.title || '', content: postData.content || '', excerpt: postData.excerpt || '', status: postData.status as PostStatus, publishedAt: postData.publishedAt ? new Date(postData.publishedAt) : undefined, organizationId: postData.organizationId || '', order: postData.order, terms: (postData as any).terms?.map((t: any) => ({ id: t.id, taxonomyId: t.taxonomyId })) || [], isLoading: false, })); // 设置编辑器内容 if (editor && postData.content) { editor.commands.setContent(postData.content); } } else if (!articleId) { // 新文章,重置为默认状态并设置默认organizationId setState((prev) => ({ ...prev, isLoading: false, organizationId: currentUser?.organizationId || '', })); } }, [postData, editor, articleId, currentUser]); // 设置加载状态 useEffect(() => { setState((prev) => ({ ...prev, isLoading })); }, [isLoading]); // 监听编辑器内容变化 useEffect(() => { if (!editor) return; const handleUpdate = () => { const content = editor.getHTML(); setState((prev) => ({ ...prev, content, hasUnsavedChanges: true, })); }; editor.on('update', handleUpdate); return () => { editor.off('update', handleUpdate); }; }, [editor]); // 自动保存 useEffect(() => { if (!state.hasUnsavedChanges) return; const timer = setTimeout(() => { // 自动保存时不显示成功消息,静默保存 save({ successMessage: undefined }); }, 30000); // 30秒自动保存 return () => clearTimeout(timer); }, [state.hasUnsavedChanges]); const updateContent = useCallback((content: string) => { setState((prev) => ({ ...prev, content, hasUnsavedChanges: true, })); }, []); const updateField = useCallback((field: K, value: EditorState[K]) => { setState((prev) => ({ ...prev, [field]: value, hasUnsavedChanges: true, })); }, []); const save = useCallback( async (options?: { newStatus?: PostStatus; successMessage?: string }) => { setState((prev) => ({ ...prev, isSaving: true })); try { const finalStatus = options?.newStatus || state.status; // 验证必填字段 - 只有发布和归档状态才需要验证 if (finalStatus === PostStatus.PUBLISHED || finalStatus === PostStatus.ARCHIVED) { if (!state.title.trim()) { toast.error('保存失败', { description: '文章标题不能为空。' }); return; } // 验证分类选择 if (taxonomiesData) { for (const taxonomy of taxonomiesData) { if (!state.terms.some((t) => t.taxonomyId === taxonomy.id)) { toast.error('保存失败', { description: `请选择一个${taxonomy.name}。` }); return; } } } } const newState = { ...state }; if (options?.newStatus) { newState.status = options.newStatus; if (options.newStatus === PostStatus.PUBLISHED && !newState.publishedAt) { newState.publishedAt = new Date(); } } // 获取当前编辑器内容 const currentContent = editor?.getHTML() || state.content; const commonData = { title: newState.title, content: currentContent, // 包含编辑器内容 excerpt: newState.excerpt, status: newState.status, order: newState.order, publishedAt: newState.publishedAt, type: PostType.ARTICLE, }; if (state.id) { // 更新现有文章 await update.mutateAsync({ where: { id: state.id }, data: { ...commonData, organizationId: newState.organizationId || undefined, terms: newState.terms.length ? { set: newState.terms.map(({ id }) => ({ id })) } : { set: [] }, }, }); // 只有在显式保存时才显示成功消息 if (options?.successMessage !== undefined) { toast.success(options.successMessage || '文章已保存'); } } else { // 创建新文章 - organizationId 现在是可选的 const effectiveOrganizationId = newState.organizationId || currentUser?.organizationId || undefined; const result = await create.mutateAsync({ data: { ...commonData, organizationId: effectiveOrganizationId, authorId: currentUser!.id, // 使用当前用户ID terms: newState.terms.length ? { connect: newState.terms.map(({ id }) => ({ id })) } : undefined, }, }); // 更新URL和状态 router.replace(`/editor?id=${result.id}`); newState.id = result.id; if (effectiveOrganizationId) { newState.organizationId = effectiveOrganizationId; } // 只有在显式保存时才显示成功消息 if (options?.successMessage !== undefined) { toast.success(options.successMessage || '文章已创建'); } } setState((prev) => ({ ...prev, ...newState, content: currentContent, hasUnsavedChanges: false, lastSaved: new Date(), })); } catch (error: any) { // 只有在显式保存时才显示错误消息 if (options?.successMessage !== undefined) { toast.error(`操作失败: ${error.message}`); } else { console.error('自动保存失败:', error); } } finally { setState((prev) => ({ ...prev, isSaving: false })); } }, [state, editor, taxonomiesData, update, create, router, currentUser], ); const reset = useCallback(() => { setState(defaultState); if (editor) { editor.commands.clearContent(); } }, [editor]); const contextValue: EditorContextValue = { state, editor, setEditor, updateContent, updateField, save, reset, }; return {children}; }