From 39ff3c48ae40db7d6a807a12f8724248f0273d9a Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 19:07:55 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor(products):=20=E6=8B=86=E5=88=86=20?= =?UTF-8?q?useProductList=20=E4=B8=BA3=E4=B8=AA=E5=AD=90Hook=EF=BC=88202?= =?UTF-8?q?=E2=86=9291=E8=A1=8C,=20-55%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useProductFiltering: 筛选逻辑(搜索/状态/时间/时长/项目/复核) - useBatchSelection: 批量选择逻辑(全选/切换/清空) - index.ts: 主入口,组合子Hook并获取数据 - 保持原路径向后兼容(目录index.ts) - 补充 smoke test 子模块导入 --- .../products/hooks/useProductList/index.ts | 91 +++++++++++++++++++ .../hooks/useProductList/useBatchSelection.ts | 42 +++++++++ .../useProductFiltering.ts} | 81 +---------------- .../src/test/pages/products/smoke.test.tsx | 2 + 4 files changed, 138 insertions(+), 78 deletions(-) create mode 100644 apps/web/src/pages/products/hooks/useProductList/index.ts create mode 100644 apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts rename apps/web/src/pages/products/hooks/{useProductList.ts => useProductList/useProductFiltering.ts} (60%) 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..0ddd8dea2 --- /dev/null +++ b/apps/web/src/pages/products/hooks/useProductList/index.ts @@ -0,0 +1,91 @@ +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 { useProductFiltering, type Filters } 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 = (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() + }) + + /* 筛选 */ + 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..5de32c7e0 --- /dev/null +++ b/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts @@ -0,0 +1,42 @@ +import { useState } 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 = () => { + 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 { + 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", () => { -- 2.54.0 From 6204d287800fff7fed3b2b1f95ad61520a84f686 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 29 Jul 2026 21:50:55 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(lint):=20=E7=A7=BB=E9=99=A4=E6=9C=AA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/products/hooks/useProductList/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/pages/products/hooks/useProductList/index.ts b/apps/web/src/pages/products/hooks/useProductList/index.ts index 0ddd8dea2..fdc30a8d2 100644 --- a/apps/web/src/pages/products/hooks/useProductList/index.ts +++ b/apps/web/src/pages/products/hooks/useProductList/index.ts @@ -1,8 +1,7 @@ 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 { useProductFiltering, type Filters } from "./useProductFiltering" +import { useProductFiltering } from "./useProductFiltering" import { useBatchSelection } from "./useBatchSelection" export type { Filters } from "./useProductFiltering" -- 2.54.0 From e204b91102f91c5ebaf8efc5eb838a906010cc57 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 00:57:57 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E5=85=A8=E9=80=89/=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E5=85=A8=E9=80=89=E4=BB=85=E6=93=8D=E4=BD=9C=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E5=8F=AF=E8=A7=81=E9=A1=B9=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E7=8A=B6=E6=80=81=E4=B8=8B=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useProductList/useBatchSelection.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts b/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts index 5de32c7e0..ee1301d35 100644 --- a/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts +++ b/apps/web/src/pages/products/hooks/useProductList/useBatchSelection.ts @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useState, useCallback } from "react" import type { ProductItem } from "../../types" export const useBatchSelection = (filteredProducts: ProductItem[]) => { @@ -9,15 +9,25 @@ export const useBatchSelection = (filteredProducts: ProductItem[]) => { const allSelected = filteredProducts.length > 0 && filteredProducts.every((p) => selectedIds.has(p.id)) - const handleSelectAll = () => { + const handleSelectAll = useCallback(() => { if (allSelected) { - setSelectedIds(new Set()) + // 仅取消选中当前可见的项,保留筛选外的选中状态 + setSelectedIds((prev) => { + const next = new Set(prev) + filteredProducts.forEach((p) => next.delete(p.id)) + return next + }) } else { - setSelectedIds(new Set(filteredProducts.map((p) => p.id))) + // 选中所有当前可见项 + setSelectedIds((prev) => { + const next = new Set(prev) + filteredProducts.forEach((p) => next.add(p.id)) + return next + }) } - } + }, [allSelected, filteredProducts]) - const handleToggleSelect = (id: string) => { + const handleToggleSelect = useCallback((id: string) => { setSelectedIds((prev) => { const next = new Set(prev) if (next.has(id)) { @@ -27,9 +37,9 @@ export const useBatchSelection = (filteredProducts: ProductItem[]) => { } return next }) - } + }, []) - const clearSelection = () => setSelectedIds(new Set()) + const clearSelection = useCallback(() => setSelectedIds(new Set()), []) return { selectedIds, -- 2.54.0 From 6526c56bf5e6d16fd28270bd91c934159d9262ea Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 00:57:58 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20products=E8=AE=A1=E7=AE=97=E4=BD=BF?= =?UTF-8?q?=E7=94=A8useMemo=E9=81=BF=E5=85=8D=E9=87=8D=E5=A4=8D=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E7=9A=84=E6=80=A7=E8=83=BD=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../products/hooks/useProductList/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/products/hooks/useProductList/index.ts b/apps/web/src/pages/products/hooks/useProductList/index.ts index fdc30a8d2..1452a46e5 100644 --- a/apps/web/src/pages/products/hooks/useProductList/index.ts +++ b/apps/web/src/pages/products/hooks/useProductList/index.ts @@ -1,3 +1,4 @@ +import { useMemo } from "react" import { useQuery } from "@tanstack/react-query" import { getProducts, type ProductItem as ApiProductItem } from "@/api/products" import { mapApiProduct } from "../../utils" @@ -21,13 +22,15 @@ export const useProductList = () => { }) // 映射为前端类型,按创建时间倒序排列,防御非数组返回 - const products = (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() - }) + 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 { -- 2.54.0