feat(ui):完成产品列表以及产品选择。
This commit is contained in:
parent
ead1cb4f71
commit
9c48156345
|
@ -6,7 +6,8 @@
|
||||||
gap: var(--mantine-space-lg);
|
gap: var(--mantine-space-lg);
|
||||||
}
|
}
|
||||||
.form-column {
|
.form-column {
|
||||||
flex-grow: 1;
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
.product-column {
|
.product-column {
|
||||||
flex-grow: 5;
|
flex-grow: 5;
|
||||||
|
|
|
@ -13,6 +13,7 @@ function App() {
|
||||||
align="stretch"
|
align="stretch"
|
||||||
justify="flex-start"
|
justify="flex-start"
|
||||||
gap="lg"
|
gap="lg"
|
||||||
|
w={450}
|
||||||
className={classes["form-column"]}
|
className={classes["form-column"]}
|
||||||
>
|
>
|
||||||
<Steps />
|
<Steps />
|
||||||
|
|
|
@ -11,7 +11,7 @@ export function LicenseCode() {
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={4}>授权码</Title>
|
<Title order={4}>授权码</Title>
|
||||||
<Tooltip label="复制授权码" position="top">
|
<Tooltip label="复制授权码" position="top">
|
||||||
<ActionIcon>
|
<ActionIcon size="lg">
|
||||||
<IconCopy size={16} />
|
<IconCopy size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -7,3 +7,10 @@
|
||||||
.title-search {
|
.title-search {
|
||||||
flex-grow: 1;
|
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 { useProducts } from "@/hooks/use-products";
|
||||||
import { IconSearch } from "@tabler/icons-react";
|
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";
|
import classes from "./ProductList.module.css";
|
||||||
|
|
||||||
export function ProductList() {
|
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 (
|
return (
|
||||||
<Paper shadow="md" p="lg" h="100%">
|
<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">
|
<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>
|
</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
|
<TextInput
|
||||||
placeholder="输入关键词检索产品"
|
placeholder="输入关键词检索产品"
|
||||||
className={classes["title-search"]}
|
className={classes["title-search"]}
|
||||||
|
value={keyword}
|
||||||
|
onChange={(event) => setKeyword(event.currentTarget.value)}
|
||||||
leftSection={<IconSearch size={16} />}
|
leftSection={<IconSearch size={16} />}
|
||||||
|
rightSection={
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
color="gray"
|
||||||
|
onClick={clearSearchKeyword}
|
||||||
|
disabled={isEmpty(keyword)}
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</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>
|
</Flex>
|
||||||
</Paper>
|
</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;
|
type ProductSearchKeyword = string | undefined | null;
|
||||||
|
|
||||||
interface Product {
|
export function useProducts(keyword: ProductSearchKeyword) {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProducts(keyword: ProductSearchKeyword): Product[] {
|
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchProducts() {
|
async function fetchProducts() {
|
||||||
const response = await fetch(`/api/products?keyword=${keyword}`);
|
const response = await fetch(`/api/products?keyword=${keyword}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setProducts(data);
|
useProductsStore.setState((state) => (state.products = data));
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchProducts();
|
fetchProducts();
|
||||||
}, [keyword]);
|
}, [keyword]);
|
||||||
|
|
||||||
return products;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user