後端
後端工作流程:
- serializer 開發
- View 開發
- URL 路由配置
- API 測試與驗證(Postman / DRF)
Serializer
使用前篇用來獲取所有文章列表的相同 PostListSerializer
1 2
| class PostListSerializer(serializers.ModelSerializer): ...
|
views
調用的 serializer 與獲取文章列表的是同一個 PostListSerializer
,但這邊會根據前端傳入的 category_id
參數來篩選文章
1 2 3 4 5 6 7 8 9 10 11 12 13
| class PostsByCategoryView(ListAPIView): serializer_class = PostListSerializer
def get_queryset(self): category_id = self.request.query_params.get('category_id') if not category_id: return Response( {"error": "需要提供 category_id 參數"}, status=status.HTTP_400_BAD_REQUEST ) if self.request.user.is_staff: return Post.objects.filter(category_id=category_id) return Post.objects.filter(category_id=category_id, status='published')
|
urls
一樣記得把這三行
1 2 3 4
| ... path('posts/published/', PublishedPostsView.as_view(), name='published-posts'), path('posts/by-category/', PostsByCategoryView.as_view(), name='posts-by-category'), path('posts/by-tag/', PostsByTagView.as_view(), name='posts-by-tag'),
|
移到這兩行之前:
1 2 3 4 5
| urlpatterns = [ path('posts/', PostListCreateView.as_view(), name='post-list'), path('posts/<slug:slug>/', PostDetailView.as_view(), name='post-detail'), ]
|
避免 Django 匹配到 'posts/'
、'posts/<slug:slug>/'
時就停止了。
前端
前端工作流程:
- api
- hooks
- 前端文章列表元件頁面
api
1 2 3 4 5 6 7 8
|
export const fetchPostsByCategory = async (categoryId: number): Promise<Post[]> => { const response = await api.get("/api/blog/posts/by-category/", { params: { category_id: categoryId }, }); return response.data; };
|
hooks
一樣用 TanStack Query 的 useQuery
來獲取文章列表,但在 queryFn
中根據 categoryId
來決定是「獲取所有已發布的文章」還是「按分類獲取文章」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export const usePostsByCategoryQuery = (categoryId: number | null) => { return useQuery({ queryKey: ["posts", "category", categoryId], queryFn: () => { if (categoryId === null) { return fetchPosts({ status: "published" }); } return fetchPostsByCategory(categoryId); }, enabled: true, }); };
|
前端頁面
在前端頁面中,因為多了需要根據選擇的分類來獲取文章,所以需要一個狀態來存儲當前選擇的分類 ID
,並根據這個 ID 來決定使用哪個 Hook
獲取文章數據
這邊也會修改到原本獲取所有文章的 usePostsQuery
,讓它可以根據是否有選擇分類來決定是獲取所有已發布的文章還是按分類獲取文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { ..., usePostsByCategoryQuery } from "@/hooks/useBlogQuery"; ...
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null); const [otherFilters, setOtherFilters] = useState<Omit<PostFilters, "category">>({});
const { data: postsByCategory = [], isLoading: isLoadingByCategory } = usePostsByCategoryQuery(selectedCategoryId); const { data: postsGeneral = [], isLoading: isLoadingGeneral, isError, error } = usePostsQuery(selectedCategoryId === null ? otherFilters : undefined); const { data: categories = [] } = useCategoriesQuery();
const posts = selectedCategoryId !== null ? postsByCategory : postsGeneral; const isLoading = selectedCategoryId !== null ? isLoadingByCategory : isLoadingGeneral;
const handleCategoryChange = (categoryId: number | null) => { setSelectedCategoryId(categoryId); };
const resetFilters = () => { setSelectedCategoryId(null); setOtherFilters({}); };
|
前端頁面中就直接在按鈕 onClick
事件中調用 handleCategoryChange
函數來更新選擇的分類 ID,並根據這個 ID 來更新獲取文章列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| return ( ... <div ref={filterRef} className='mb-8'> <div className='flex items-center mb-4'> <Filter className='w-4 h-4 mr-2' /> <span>分類篩選</span> </div> <div className='flex flex-wrap gap-2'> <Button variant={selectedCategoryId === null ? "default" : "outline"} onClick={() => handleCategoryChange(null)} size='sm' className='rounded-full'> 全部 </Button> {categories.map((category) => ( <Button key={category.id} variant={selectedCategoryId === category.id ? "default" : "outline"} onClick={() => handleCategoryChange(category.id)} size='sm' className='rounded-full'> {category.name} </Button> ))} </div> {selectedCategoryId !== null && ( <Button variant='ghost' onClick={resetFilters} size='sm' className='mt-4'> <RefreshCw className='w-4 h-4 mr-2' /> 重設篩選 </Button> )} </div> )
|
若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~
留言版