feat(ui):完成产品列表以及产品选择。
This commit is contained in:
		| @@ -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; | ||||
|   | ||||
| @@ -13,6 +13,7 @@ function App() { | ||||
|         align="stretch" | ||||
|         justify="flex-start" | ||||
|         gap="lg" | ||||
|         w={450} | ||||
|         className={classes["form-column"]} | ||||
|       > | ||||
|         <Steps /> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ export function LicenseCode() { | ||||
|         <Group justify="space-between"> | ||||
|           <Title order={4}>授权码</Title> | ||||
|           <Tooltip label="复制授权码" position="top"> | ||||
|             <ActionIcon> | ||||
|             <ActionIcon size="lg"> | ||||
|               <IconCopy size={16} /> | ||||
|             </ActionIcon> | ||||
|           </Tooltip> | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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<string>(""); | ||||
|   const [selected, products, selectAll, unselectAll] = useProductsStore((state) => [ | ||||
|     state.selctedProducts, | ||||
|     state.products, | ||||
|     state.selectAll, | ||||
|     state.unselectAll, | ||||
|   ]); | ||||
|   const clearSearchKeyword = useCallback(() => setKeyword(""), []); | ||||
|   useProducts(keyword); | ||||
|  | ||||
|   return ( | ||||
|     <Paper shadow="md" p="lg" h="100%"> | ||||
|       <Flex direction="column" justify="flex-start" align="stretch" gap="md"> | ||||
|       <Flex direction="column" justify="flex-start" align="stretch" gap="md" h="100%"> | ||||
|         <Flex direction="row" justify={"space-between"} align={"center"} gap="sm"> | ||||
|           <Title order={4} className={classes["title"]}> | ||||
|             产品列表 | ||||
|           <Title order={4} className={classes["title"]} maw={650}> | ||||
|             产品列表{not(isEmpty(selected)) && `(已选择 ${selected.length} 个产品)`} | ||||
|           </Title> | ||||
|           <Tooltip label="取消当前的所有选择"> | ||||
|             <ActionIcon size="lg" color="red" onClick={unselectAll} disabled={isEmpty(selected)}> | ||||
|               <IconDeselect size={16} /> | ||||
|             </ActionIcon> | ||||
|           </Tooltip> | ||||
|           <Tooltip label="选择当前所有"> | ||||
|             <ActionIcon size="lg" onClick={selectAll}> | ||||
|               <IconListCheck size={16} /> | ||||
|             </ActionIcon> | ||||
|           </Tooltip> | ||||
|           <TextInput | ||||
|             placeholder="输入关键词检索产品" | ||||
|             className={classes["title-search"]} | ||||
|             value={keyword} | ||||
|             onChange={(event) => setKeyword(event.currentTarget.value)} | ||||
|             leftSection={<IconSearch size={16} />} | ||||
|             rightSection={ | ||||
|               <ActionIcon | ||||
|                 variant="transparent" | ||||
|                 color="gray" | ||||
|                 onClick={clearSearchKeyword} | ||||
|                 disabled={isEmpty(keyword)} | ||||
|               > | ||||
|                 <IconX size={16} /> | ||||
|               </ActionIcon> | ||||
|             } | ||||
|           /> | ||||
|         </Flex> | ||||
|         <Flex | ||||
|           direction="row" | ||||
|           justify="flex-start" | ||||
|           align="flex-start" | ||||
|           wrap="wrap" | ||||
|           gap="sm" | ||||
|           className={classes["product-list-container"]} | ||||
|         > | ||||
|           {products.map((product) => ( | ||||
|             <ProductItem key={product.id} id={product.id} name={product.name} /> | ||||
|           ))} | ||||
|         </Flex> | ||||
|       </Flex> | ||||
|     </Paper> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| 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 ( | ||||
|     <Box px="sm" py="xs" bg={selected ? "green" : "gray"} onClick={handleSelectAction}> | ||||
|       <Text size="xs" c={selected ? "white" : "gray"}> | ||||
|         {props.name} | ||||
|       </Text> | ||||
|     </Box> | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										30
									
								
								license_ui/src/hooks/use-products-store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								license_ui/src/hooks/use-products-store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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<ProductsStore>((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 }); | ||||
|   }, | ||||
| })); | ||||
| @@ -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<Product[]>([]); | ||||
|  | ||||
| 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; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user