diff --git a/apps/web/src/pages/products/hooks/useProductList/index.ts b/apps/web/src/pages/products/hooks/useProductList/index.ts new file mode 100644 index 000000000..1452a46e5 --- /dev/null +++ b/apps/web/src/pages/products/hooks/useProductList/index.ts @@ -0,0 +1,93 @@ +import { useMemo } from "react" +import { useQuery } from "@tanstack/react-query" +import { getProducts, type ProductItem as ApiProductItem } from "@/api/products" +import { mapApiProduct } from "../../utils" +import { useProductFiltering } from "./useProductFiltering" +import { useBatchSelection } from "./useBatchSelection" + +export type { Filters } from "./useProductFiltering" + +export const useProductList = () => { + /* ── 获取成品列表 ── */ + const { + data: apiProducts = [], + isLoading, + isError, + error, + refetch, + } = useQuery({ + queryKey: ["products"], + queryFn: () => getProducts(), + staleTime: 30_000, + }) + + // 映射为前端类型,按创建时间倒序排列,防御非数组返回 + const products = useMemo( + () => + (Array.isArray(apiProducts) ? apiProducts : []).map(mapApiProduct).sort((a, b) => { + if (!a.date || a.date === "—") return 1 + if (!b.date || b.date === "—") return -1 + return new Date(b.date).getTime() - new Date(a.date).getTime() + }), + [apiProducts], + ) + + /* 筛选 */ + const { + searchText, + setSearchText, + filterStatus, + setFilterStatus, + filterTime, + setFilterTime, + filterDuration, + setFilterDuration, + filterProject, + setFilterProject, + filterReviewStatus, + setFilterReviewStatus, + filteredProducts, + projectOptions, + } = useProductFiltering(products) + + /* 批量选择 */ + const { + selectedIds, + batchMode, + allSelected, + handleSelectAll, + handleToggleSelect, + clearSelection, + } = useBatchSelection(filteredProducts) + + return { + // 数据 + products, + filteredProducts, + isLoading, + isError, + error, + refetch, + // 筛选 + searchText, + setSearchText, + filterStatus, + setFilterStatus, + filterTime, + setFilterTime, + filterDuration, + setFilterDuration, + filterProject, + setFilterProject, + filterReviewStatus, + setFilterReviewStatus, + projectOptions, + // 批量选择 + selectedIds, + batchMode, + allSelected, + handleSelectAll, + handleToggleSelect, + clearSelection, + } +} diff --git a/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts b/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts new file mode 100644 index 000000000..ee1301d35 --- /dev/null +++ b/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts @@ -0,0 +1,52 @@ +import { useState, useCallback } from "react" +import type { ProductItem } from "../../types" + +export const useBatchSelection = (filteredProducts: ProductItem[]) => { + const [selectedIds, setSelectedIds] = useState>(new Set()) + + const batchMode = selectedIds.size > 0 + + const allSelected = + filteredProducts.length > 0 && filteredProducts.every((p) => selectedIds.has(p.id)) + + const handleSelectAll = useCallback(() => { + if (allSelected) { + // 仅取消选中当前可见的项,保留筛选外的选中状态 + setSelectedIds((prev) => { + const next = new Set(prev) + filteredProducts.forEach((p) => next.delete(p.id)) + return next + }) + } else { + // 选中所有当前可见项 + setSelectedIds((prev) => { + const next = new Set(prev) + filteredProducts.forEach((p) => next.add(p.id)) + return next + }) + } + }, [allSelected, filteredProducts]) + + const handleToggleSelect = useCallback((id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev) + if (next.has(id)) { + next.delete(id) + } else { + next.add(id) + } + return next + }) + }, []) + + const clearSelection = useCallback(() => setSelectedIds(new Set()), []) + + return { + selectedIds, + batchMode, + allSelected, + handleSelectAll, + handleToggleSelect, + clearSelection, + } +} diff --git a/apps/web/src/pages/products/hooks/useProductList.ts b/apps/web/src/pages/products/hooks/useProductList/useProductFiltering.ts similarity index 60% rename from apps/web/src/pages/products/hooks/useProductList.ts rename to apps/web/src/pages/products/hooks/useProductList/useProductFiltering.ts index eaa9b49f1..c7b40ccdc 100644 --- a/apps/web/src/pages/products/hooks/useProductList.ts +++ b/apps/web/src/pages/products/hooks/useProductList/useProductFiltering.ts @@ -1,8 +1,5 @@ import { useMemo, useState } from "react" -import { useQuery } from "@tanstack/react-query" -import { getProducts, type ProductItem as ApiProductItem } from "@/api/products" -import type { ProductItem } from "../types" -import { mapApiProduct } from "../utils" +import type { ProductItem } from "../../types" /** 筛选选项类型 */ export interface Filters { @@ -27,32 +24,7 @@ const getProjectOptions = (products: ProductItem[]) => label: name as string, })) -export const useProductList = () => { - /* ── 获取成品列表 ── */ - const { - data: apiProducts = [], - isLoading, - isError, - error, - refetch, - } = useQuery({ - queryKey: ["products"], - queryFn: () => getProducts(), - staleTime: 30_000, - }) - - // 映射为前端类型,按创建时间倒序排列,防御非数组返回 - const products = useMemo(() => { - const list = (Array.isArray(apiProducts) ? apiProducts : []).map(mapApiProduct) - // 按创建时间倒序(最新的在最前面),无时间的排最后 - return list.sort((a, b) => { - if (!a.date || a.date === "—") return 1 - if (!b.date || b.date === "—") return -1 - return new Date(b.date).getTime() - new Date(a.date).getTime() - }) - }, [apiProducts]) - - /* 筛选 */ +export const useProductFiltering = (products: ProductItem[]) => { const [searchText, setSearchText] = useState("") const [filterStatus, setFilterStatus] = useState("all") const [filterTime, setFilterTime] = useState("all") @@ -60,12 +32,6 @@ export const useProductList = () => { const [filterProject, setFilterProject] = useState("all") const [filterReviewStatus, setFilterReviewStatus] = useState("all") - /* 批量选择 */ - const [selectedIds, setSelectedIds] = useState>(new Set()) - - /* 派生数据 */ - const batchMode = selectedIds.size > 0 - const filteredProducts = useMemo(() => { let list = products @@ -142,42 +108,7 @@ export const useProductList = () => { const projectOptions = useMemo(() => getProjectOptions(products), [products]) - /* 全选 */ - const allSelected = - filteredProducts.length > 0 && filteredProducts.every((p) => selectedIds.has(p.id)) - - const handleSelectAll = () => { - if (allSelected) { - setSelectedIds(new Set()) - } else { - setSelectedIds(new Set(filteredProducts.map((p) => p.id))) - } - } - - /* 切换单个选择 */ - const handleToggleSelect = (id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev) - if (next.has(id)) { - next.delete(id) - } else { - next.add(id) - } - return next - }) - } - - const clearSelection = () => setSelectedIds(new Set()) - return { - // 数据 - products, - filteredProducts, - isLoading, - isError, - error, - refetch, - // 筛选 searchText, setSearchText, filterStatus, @@ -190,13 +121,7 @@ export const useProductList = () => { setFilterProject, filterReviewStatus, setFilterReviewStatus, + filteredProducts, projectOptions, - // 批量选择 - selectedIds, - batchMode, - allSelected, - handleSelectAll, - handleToggleSelect, - clearSelection, } } diff --git a/apps/web/src/test/pages/products/smoke.test.tsx b/apps/web/src/test/pages/products/smoke.test.tsx index fbe1b7dbc..dddb71f58 100644 --- a/apps/web/src/test/pages/products/smoke.test.tsx +++ b/apps/web/src/test/pages/products/smoke.test.tsx @@ -21,6 +21,8 @@ import "@/pages/products/components/VideoPlayer" // Hooks import "@/pages/products/hooks/useProductList" +import "@/pages/products/hooks/useProductList/useProductFiltering" +import "@/pages/products/hooks/useProductList/useBatchSelection" import "@/pages/products/hooks/useProductActions" describe("ProductLibrary module smoke test", () => {