feat(ui):完成产品列表以及产品选择。

This commit is contained in:
徐涛 2024-04-05 15:32:48 +08:00
parent ead1cb4f71
commit 9c48156345
7 changed files with 128 additions and 19 deletions

View File

@ -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;

View File

@ -13,6 +13,7 @@ function App() {
align="stretch"
justify="flex-start"
gap="lg"
w={450}
className={classes["form-column"]}
>
<Steps />

View File

@ -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>

View File

@ -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;
}

View File

@ -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>
);
}

View 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 });
},
}));

View File

@ -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;
}