diff --git a/license_ui/src/App.module.css b/license_ui/src/App.module.css index e4154db..cb2671c 100644 --- a/license_ui/src/App.module.css +++ b/license_ui/src/App.module.css @@ -6,7 +6,8 @@ gap: var(--mantine-space-lg); } .form-column { - flex-grow: 1; + flex-shrink: 0; + flex-grow: 0; } .product-column { flex-grow: 5; diff --git a/license_ui/src/App.tsx b/license_ui/src/App.tsx index a9f142d..ef8653d 100644 --- a/license_ui/src/App.tsx +++ b/license_ui/src/App.tsx @@ -13,6 +13,7 @@ function App() { align="stretch" justify="flex-start" gap="lg" + w={450} className={classes["form-column"]} > diff --git a/license_ui/src/components/LicenseCode.tsx b/license_ui/src/components/LicenseCode.tsx index b19138d..169cd72 100644 --- a/license_ui/src/components/LicenseCode.tsx +++ b/license_ui/src/components/LicenseCode.tsx @@ -11,7 +11,7 @@ export function LicenseCode() { 授权码 - + diff --git a/license_ui/src/components/ProductList.module.css b/license_ui/src/components/ProductList.module.css index befc017..a6e9e0c 100644 --- a/license_ui/src/components/ProductList.module.css +++ b/license_ui/src/components/ProductList.module.css @@ -7,3 +7,10 @@ .title-search { flex-grow: 1; } +.product-list-container { + align-content: flex-start; + flex-grow: 1; + overflow-x: hidden; + overflow-y: auto; + min-height: 0; +} diff --git a/license_ui/src/components/ProductList.tsx b/license_ui/src/components/ProductList.tsx index 4f5c176..08209a0 100644 --- a/license_ui/src/components/ProductList.tsx +++ b/license_ui/src/components/ProductList.tsx @@ -1,22 +1,100 @@ -import { Flex, Paper, TextInput, Title } from "@mantine/core"; -import { IconSearch } from "@tabler/icons-react"; +import { useProducts } from "@/hooks/use-products"; +import { useProductsStore } from "@/hooks/use-products-store"; +import { ActionIcon, Box, Flex, Paper, Text, TextInput, Title, Tooltip } from "@mantine/core"; +import { IconDeselect, IconListCheck, IconSearch, IconX } from "@tabler/icons-react"; +import { isEmpty, not } from "ramda"; +import { useCallback, useMemo, useState } from "react"; import classes from "./ProductList.module.css"; export function ProductList() { + const [keyword, setKeyword] = useState(""); + const [selected, products, selectAll, unselectAll] = useProductsStore((state) => [ + state.selctedProducts, + state.products, + state.selectAll, + state.unselectAll, + ]); + const clearSearchKeyword = useCallback(() => setKeyword(""), []); + useProducts(keyword); + return ( - + - - 产品列表 + <Title order={4} className={classes["title"]} maw={650}> + 产品列表{not(isEmpty(selected)) && `(已选择 ${selected.length} 个产品)`} + + + + + + + + + + setKeyword(event.currentTarget.value)} leftSection={} + rightSection={ + + + + } /> + + {products.map((product) => ( + + ))} + ); } + +interface ProductItemProps { + id: string; + name: string; +} + +function ProductItem(props: ProductItemProps) { + const [selectedProducts, append, remove] = useProductsStore((state) => [ + state.selctedProducts, + state.append, + state.remove, + ]); + const selected = useMemo(() => selectedProducts.includes(props.id), [props.id, selectedProducts]); + + const handleSelectAction = useCallback(() => { + if (selected) { + remove(props.id); + } else { + append(props.id); + } + }, [props.id, selected, append, remove]); + + return ( + + + {props.name} + + + ); +} diff --git a/license_ui/src/hooks/use-products-store.ts b/license_ui/src/hooks/use-products-store.ts new file mode 100644 index 0000000..35ef1bc --- /dev/null +++ b/license_ui/src/hooks/use-products-store.ts @@ -0,0 +1,30 @@ +import { concat, pluck, uniq } from "ramda"; +import { create } from "zustand"; + +interface Product { + id: string; + name: string; +} + +interface ProductsStore { + products: Product[]; + selctedProducts: string[]; + append: (code: string) => void; + remove: (code: string) => void; + unselectAll: () => void; + selectAll: () => void; +} + +export const useProductsStore = create((set, get) => ({ + products: [], + selctedProducts: [], + append: (code: string) => set((state) => ({ selctedProducts: [...state.selctedProducts, code] })), + remove: (code: string) => + set((state) => ({ selctedProducts: state.selctedProducts.filter((item) => item !== code) })), + unselectAll: () => set({ selctedProducts: [] }), + selectAll: () => { + const products = pluck("id", get().products); + const selectedProducts = uniq(concat(get().selctedProducts, products)); + set({ selctedProducts: selectedProducts }); + }, +})); diff --git a/license_ui/src/hooks/use-products.ts b/license_ui/src/hooks/use-products.ts index da6f394..6844ca1 100644 --- a/license_ui/src/hooks/use-products.ts +++ b/license_ui/src/hooks/use-products.ts @@ -1,24 +1,16 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useProductsStore } from "./use-products-store"; type ProductSearchKeyword = string | undefined | null; -interface Product { - id: string; - name: string; -} - -export function useProducts(keyword: ProductSearchKeyword): Product[] { - const [products, setProducts] = useState([]); - +export function useProducts(keyword: ProductSearchKeyword) { useEffect(() => { async function fetchProducts() { const response = await fetch(`/api/products?keyword=${keyword}`); const data = await response.json(); - setProducts(data); + useProductsStore.setState((state) => (state.products = data)); } fetchProducts(); }, [keyword]); - - return products; }