feat(ui):完成产品列表以及产品选择。
This commit is contained in:
parent
ead1cb4f71
commit
9c48156345
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user