feat(ui):完成大部分功能。
This commit is contained in:
		| @@ -1,5 +1,6 @@ | ||||
| .container { | ||||
|   flex-grow: 1; | ||||
|   overflow-y: hidden; | ||||
| } | ||||
| .license-code-area { | ||||
|   flex-grow: 1; | ||||
|   | ||||
| @@ -1,22 +1,34 @@ | ||||
| import { useLicenseCode } from "@/hooks/use_license_code"; | ||||
| import { useLicenseCodeStore } from "@/hooks/use_license_code_store"; | ||||
| import { ActionIcon, Box, Flex, Group, Paper, Title, Tooltip } from "@mantine/core"; | ||||
| import { notifications } from "@mantine/notifications"; | ||||
| import { IconCopy } from "@tabler/icons-react"; | ||||
| import { isEmpty, not } from "ramda"; | ||||
| import classes from "./LicenseCode.module.css"; | ||||
|  | ||||
| export function LicenseCode() { | ||||
|   const [licenseCode] = useLicenseCode(); | ||||
|   const licenseCode = useLicenseCodeStore((state) => state.licenceCode); | ||||
|   const copyLicenseCode = async () => { | ||||
|     if (not(isEmpty(licenseCode))) { | ||||
|       await navigator.clipboard.writeText(licenseCode); | ||||
|       notifications.show({ | ||||
|         title: "授权码已复制", | ||||
|         color: "green", | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Paper shadow="md" p="lg" className={classes["container"]}> | ||||
|       <Flex direction="column" justify="flex-start" align="stretch" gap="md"> | ||||
|       <Flex direction="column" justify="flex-start" align="stretch" gap="md" h="100%"> | ||||
|         <Group justify="space-between"> | ||||
|           <Title order={4}>授权码</Title> | ||||
|           <Tooltip label="复制授权码" position="top"> | ||||
|             <ActionIcon size="lg"> | ||||
|             <ActionIcon size="lg" onClick={copyLicenseCode}> | ||||
|               <IconCopy size={16} /> | ||||
|             </ActionIcon> | ||||
|           </Tooltip> | ||||
|         </Group> | ||||
|         <Box className={classes["license-code-area"]} c="green"> | ||||
|         <Box className={classes["license-code-area"]} c="green" fz="xs"> | ||||
|           {licenseCode} | ||||
|         </Box> | ||||
|       </Flex> | ||||
|   | ||||
| @@ -1,63 +1,104 @@ | ||||
| import { licenseValidLength } from "@/constants"; | ||||
| import { useLicenseCodeStore } from "@/hooks/use_license_code_store"; | ||||
| import { useProductsStore } from "@/hooks/use_products_store"; | ||||
| import type { LicenseInfoForm } from "@/types"; | ||||
| import { Button, Flex, Group, Paper, Select, TextInput, Title } from "@mantine/core"; | ||||
| import { useForm } from "@mantine/form"; | ||||
| import { notifications } from "@mantine/notifications"; | ||||
| import { IconAt, IconCalendar, IconTag, IconUser } from "@tabler/icons-react"; | ||||
| import { pluck } from "ramda"; | ||||
|  | ||||
| const licenseValidLength = [ | ||||
|   { value: 1, label: "一年" }, | ||||
|   { value: 2, label: "两年" }, | ||||
|   { value: 3, label: "三年" }, | ||||
|   { value: 4, label: "四年" }, | ||||
|   { value: 5, label: "五年" }, | ||||
|   { value: 6, label: "六年" }, | ||||
|   { value: 7, label: "七年" }, | ||||
|   { value: 8, label: "八年" }, | ||||
|   { value: 9, label: "九年" }, | ||||
|   { value: 10, label: "十年" }, | ||||
|   { value: 15, label: "十五年" }, | ||||
|   { value: 20, label: "二十年" }, | ||||
|   { value: 25, label: "二十五年" }, | ||||
|   { value: 30, label: "三十年" }, | ||||
|   { value: 35, label: "三十五年" }, | ||||
|   { value: 40, label: "四十年" }, | ||||
|   { value: 45, label: "四十五年" }, | ||||
|   { value: 50, label: "五十年" }, | ||||
| ]; | ||||
| import dayjs from "dayjs"; | ||||
| import { find, isEmpty, pluck, prop, propEq } from "ramda"; | ||||
| import { useCallback } from "react"; | ||||
|  | ||||
| export function LicenseForm() { | ||||
|   const form = useForm<LicenseInfoForm>({ | ||||
|     initialValues: { | ||||
|       licenseName: "", | ||||
|       assigneeName: "", | ||||
|       assigneeEmail: "", | ||||
|       validLength: licenseValidLength[0].label, | ||||
|     }, | ||||
|     validate: { | ||||
|       licenseName: (value) => (isEmpty(value) ? "授权名称不能为空" : null), | ||||
|       assigneeName: (value) => (isEmpty(value) ? "被授权人不能为空" : null), | ||||
|     }, | ||||
|   }); | ||||
|   const selectedProducts = useProductsStore((state) => state.selectedProducts); | ||||
|   const updateLicenseCode = useLicenseCodeStore((state) => state.setLicenseCode); | ||||
|   const licenseGenAction = useCallback( | ||||
|     async (formValue: LicenseInfoForm) => { | ||||
|       const license = await generateLicense(formValue, selectedProducts); | ||||
|       updateLicenseCode(license); | ||||
|     }, | ||||
|     [selectedProducts, updateLicenseCode] | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Paper shadow="md" p="lg"> | ||||
|       <form> | ||||
|       <form onSubmit={form.onSubmit(licenseGenAction)}> | ||||
|         <Flex direction="column" gap="md"> | ||||
|           <Title order={4}>授权信息</Title> | ||||
|           <TextInput | ||||
|             label="授权名称" | ||||
|             placeholder="请输入授权名称" | ||||
|             required | ||||
|             withAsterisk | ||||
|             leftSection={<IconTag size={16} />} | ||||
|             {...form.getInputProps("licenseName")} | ||||
|           /> | ||||
|           <TextInput | ||||
|             label="被授权人" | ||||
|             placeholder="请输入授权人名称" | ||||
|             required | ||||
|             withAsterisk | ||||
|             leftSection={<IconUser size={16} />} | ||||
|             {...form.getInputProps("assigneeName")} | ||||
|           /> | ||||
|           <TextInput | ||||
|             label="被授权人Email" | ||||
|             placeholder="请输入授权人Email" | ||||
|             leftSection={<IconAt size={16} />} | ||||
|             {...form.getInputProps("assigneeEmail")} | ||||
|           /> | ||||
|           <Select | ||||
|             label="授权有效时长" | ||||
|             placeholder="请选择授权有效时长" | ||||
|             data={pluck("label", licenseValidLength)} | ||||
|             defaultValue={licenseValidLength[0].label} | ||||
|             leftSection={<IconCalendar size={16} />} | ||||
|             {...form.getInputProps("validLength")} | ||||
|           /> | ||||
|           <Group justify="flex-end"> | ||||
|             <Button>生成授权</Button> | ||||
|             <Button type="submit">生成授权</Button> | ||||
|           </Group> | ||||
|         </Flex> | ||||
|       </form> | ||||
|     </Paper> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| async function generateLicense(form: LicenseInfoForm, selectedProducts: string[]) { | ||||
|   const validYears = find(propEq(form.validLength, "label"), licenseValidLength)?.value ?? 0; | ||||
|   const now = dayjs(); | ||||
|   const validDays = now.add(validYears, "year").diff(now, "day"); | ||||
|   if (isEmpty(selectedProducts)) { | ||||
|     notifications.show({ | ||||
|       title: "至少需要选择一个产品", | ||||
|       color: "red", | ||||
|     }); | ||||
|     return ""; | ||||
|   } | ||||
|   const requestBody = { | ||||
|     licenseeName: form.licenseName, | ||||
|     assigneeName: form.assigneeName, | ||||
|     assigneeEmail: form.assigneeEmail, | ||||
|     validDays, | ||||
|     requestProducts: selectedProducts, | ||||
|   }; | ||||
|   const response = await fetch("/api/license", { | ||||
|     method: "POST", | ||||
|     headers: { | ||||
|       "Content-Type": "application/json", | ||||
|     }, | ||||
|     body: JSON.stringify(requestBody), | ||||
|   }); | ||||
|   const data = await response.json(); | ||||
|   return prop<"license", { license: string }>("license", data); | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import classes from "./ProductList.module.css"; | ||||
| export function ProductList() { | ||||
|   const [keyword, setKeyword] = useState<string>(""); | ||||
|   const [selected, products, selectAll, unselectAll] = useProductsStore((state) => [ | ||||
|     state.selctedProducts, | ||||
|     state.selectedProducts, | ||||
|     state.products, | ||||
|     state.selectAll, | ||||
|     state.unselectAll, | ||||
| @@ -76,7 +76,7 @@ interface ProductItemProps { | ||||
|  | ||||
| function ProductItem(props: ProductItemProps) { | ||||
|   const [selectedProducts, append, remove] = useProductsStore((state) => [ | ||||
|     state.selctedProducts, | ||||
|     state.selectedProducts, | ||||
|     state.append, | ||||
|     state.remove, | ||||
|   ]); | ||||
|   | ||||
							
								
								
									
										20
									
								
								license_ui/src/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								license_ui/src/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| export const licenseValidLength = [ | ||||
|   { value: 1, label: "一年" }, | ||||
|   { value: 2, label: "两年" }, | ||||
|   { value: 3, label: "三年" }, | ||||
|   { value: 4, label: "四年" }, | ||||
|   { value: 5, label: "五年" }, | ||||
|   { value: 6, label: "六年" }, | ||||
|   { value: 7, label: "七年" }, | ||||
|   { value: 8, label: "八年" }, | ||||
|   { value: 9, label: "九年" }, | ||||
|   { value: 10, label: "十年" }, | ||||
|   { value: 15, label: "十五年" }, | ||||
|   { value: 20, label: "二十年" }, | ||||
|   { value: 25, label: "二十五年" }, | ||||
|   { value: 30, label: "三十年" }, | ||||
|   { value: 35, label: "三十五年" }, | ||||
|   { value: 40, label: "四十年" }, | ||||
|   { value: 45, label: "四十五年" }, | ||||
|   { value: 50, label: "五十年" }, | ||||
| ]; | ||||
| @@ -1,6 +0,0 @@ | ||||
| import { useState } from "react"; | ||||
|  | ||||
| export function useLicenseCode(): [string, (licenseCode: string) => void] { | ||||
|   const [licenseCode, setLicenseCode] = useState<string>(""); | ||||
|   return [licenseCode, setLicenseCode]; | ||||
| } | ||||
							
								
								
									
										11
									
								
								license_ui/src/hooks/use_license_code_store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								license_ui/src/hooks/use_license_code_store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { create } from "zustand"; | ||||
|  | ||||
| interface LicenseCodeStore { | ||||
|   licenceCode?: string; | ||||
|   setLicenseCode: (code: string) => void; | ||||
| } | ||||
|  | ||||
| export const useLicenseCodeStore = create<LicenseCodeStore>((set) => ({ | ||||
|   licenceCode: undefined, | ||||
|   setLicenseCode: (code) => set({ licenceCode: code }), | ||||
| })); | ||||
| @@ -8,7 +8,7 @@ interface Product { | ||||
|  | ||||
| interface ProductsStore { | ||||
|   products: Product[]; | ||||
|   selctedProducts: string[]; | ||||
|   selectedProducts: string[]; | ||||
|   append: (code: string) => void; | ||||
|   remove: (code: string) => void; | ||||
|   unselectAll: () => void; | ||||
| @@ -17,14 +17,15 @@ interface ProductsStore { | ||||
|  | ||||
| export const useProductsStore = create<ProductsStore>((set, get) => ({ | ||||
|   products: [], | ||||
|   selctedProducts: [], | ||||
|   append: (code: string) => set((state) => ({ selctedProducts: [...state.selctedProducts, code] })), | ||||
|   selectedProducts: [], | ||||
|   append: (code: string) => | ||||
|     set((state) => ({ selectedProducts: [...state.selectedProducts, code] })), | ||||
|   remove: (code: string) => | ||||
|     set((state) => ({ selctedProducts: state.selctedProducts.filter((item) => item !== code) })), | ||||
|   unselectAll: () => set({ selctedProducts: [] }), | ||||
|     set((state) => ({ selectedProducts: state.selectedProducts.filter((item) => item !== code) })), | ||||
|   unselectAll: () => set({ selectedProducts: [] }), | ||||
|   selectAll: () => { | ||||
|     const products = pluck("id", get().products); | ||||
|     const selectedProducts = uniq(concat(get().selctedProducts, products)); | ||||
|     set({ selctedProducts: selectedProducts }); | ||||
|     const selectedProducts = uniq(concat(get().selectedProducts, products)); | ||||
|     set({ selectedProducts: selectedProducts }); | ||||
|   }, | ||||
| })); | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import { MantineProvider } from "@mantine/core"; | ||||
| import "@mantine/core/styles.css"; | ||||
| import { Notifications } from "@mantine/notifications"; | ||||
| import "@mantine/notifications/styles.css"; | ||||
| import React from "react"; | ||||
| import ReactDOM from "react-dom/client"; | ||||
| import App from "./App.tsx"; | ||||
| @@ -9,6 +11,7 @@ import { theme } from "./theme"; | ||||
| ReactDOM.createRoot(document.getElementById("root")!).render( | ||||
|   <React.StrictMode> | ||||
|     <MantineProvider defaultColorScheme="dark" theme={theme}> | ||||
|       <Notifications /> | ||||
|       <App /> | ||||
|     </MantineProvider> | ||||
|   </React.StrictMode> | ||||
|   | ||||
							
								
								
									
										6
									
								
								license_ui/src/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								license_ui/src/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| export interface LicenseInfoForm { | ||||
|   licenseName: string; | ||||
|   assigneeName: string; | ||||
|   assigneeEmail: string; | ||||
|   validLength: string; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user