feat(ui):完成大部分功能。
This commit is contained in:
parent
59c8a316fe
commit
f74f04bd4c
|
@ -1,5 +1,6 @@
|
||||||
.container {
|
.container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
.license-code-area {
|
.license-code-area {
|
||||||
flex-grow: 1;
|
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 { ActionIcon, Box, Flex, Group, Paper, Title, Tooltip } from "@mantine/core";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IconCopy } from "@tabler/icons-react";
|
import { IconCopy } from "@tabler/icons-react";
|
||||||
|
import { isEmpty, not } from "ramda";
|
||||||
import classes from "./LicenseCode.module.css";
|
import classes from "./LicenseCode.module.css";
|
||||||
|
|
||||||
export function LicenseCode() {
|
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 (
|
return (
|
||||||
<Paper shadow="md" p="lg" className={classes["container"]}>
|
<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">
|
<Group justify="space-between">
|
||||||
<Title order={4}>授权码</Title>
|
<Title order={4}>授权码</Title>
|
||||||
<Tooltip label="复制授权码" position="top">
|
<Tooltip label="复制授权码" position="top">
|
||||||
<ActionIcon size="lg">
|
<ActionIcon size="lg" onClick={copyLicenseCode}>
|
||||||
<IconCopy size={16} />
|
<IconCopy size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Group>
|
</Group>
|
||||||
<Box className={classes["license-code-area"]} c="green">
|
<Box className={classes["license-code-area"]} c="green" fz="xs">
|
||||||
{licenseCode}
|
{licenseCode}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</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 { 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 { IconAt, IconCalendar, IconTag, IconUser } from "@tabler/icons-react";
|
||||||
import { pluck } from "ramda";
|
import dayjs from "dayjs";
|
||||||
|
import { find, isEmpty, pluck, prop, propEq } from "ramda";
|
||||||
const licenseValidLength = [
|
import { useCallback } from "react";
|
||||||
{ 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: "五十年" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function LicenseForm() {
|
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 (
|
return (
|
||||||
<Paper shadow="md" p="lg">
|
<Paper shadow="md" p="lg">
|
||||||
<form>
|
<form onSubmit={form.onSubmit(licenseGenAction)}>
|
||||||
<Flex direction="column" gap="md">
|
<Flex direction="column" gap="md">
|
||||||
<Title order={4}>授权信息</Title>
|
<Title order={4}>授权信息</Title>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="授权名称"
|
label="授权名称"
|
||||||
placeholder="请输入授权名称"
|
placeholder="请输入授权名称"
|
||||||
required
|
withAsterisk
|
||||||
leftSection={<IconTag size={16} />}
|
leftSection={<IconTag size={16} />}
|
||||||
|
{...form.getInputProps("licenseName")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="被授权人"
|
label="被授权人"
|
||||||
placeholder="请输入授权人名称"
|
placeholder="请输入授权人名称"
|
||||||
required
|
withAsterisk
|
||||||
leftSection={<IconUser size={16} />}
|
leftSection={<IconUser size={16} />}
|
||||||
|
{...form.getInputProps("assigneeName")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="被授权人Email"
|
label="被授权人Email"
|
||||||
placeholder="请输入授权人Email"
|
placeholder="请输入授权人Email"
|
||||||
leftSection={<IconAt size={16} />}
|
leftSection={<IconAt size={16} />}
|
||||||
|
{...form.getInputProps("assigneeEmail")}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="授权有效时长"
|
label="授权有效时长"
|
||||||
placeholder="请选择授权有效时长"
|
placeholder="请选择授权有效时长"
|
||||||
data={pluck("label", licenseValidLength)}
|
data={pluck("label", licenseValidLength)}
|
||||||
defaultValue={licenseValidLength[0].label}
|
|
||||||
leftSection={<IconCalendar size={16} />}
|
leftSection={<IconCalendar size={16} />}
|
||||||
|
{...form.getInputProps("validLength")}
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button>生成授权</Button>
|
<Button type="submit">生成授权</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Flex>
|
</Flex>
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</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() {
|
export function ProductList() {
|
||||||
const [keyword, setKeyword] = useState<string>("");
|
const [keyword, setKeyword] = useState<string>("");
|
||||||
const [selected, products, selectAll, unselectAll] = useProductsStore((state) => [
|
const [selected, products, selectAll, unselectAll] = useProductsStore((state) => [
|
||||||
state.selctedProducts,
|
state.selectedProducts,
|
||||||
state.products,
|
state.products,
|
||||||
state.selectAll,
|
state.selectAll,
|
||||||
state.unselectAll,
|
state.unselectAll,
|
||||||
|
@ -76,7 +76,7 @@ interface ProductItemProps {
|
||||||
|
|
||||||
function ProductItem(props: ProductItemProps) {
|
function ProductItem(props: ProductItemProps) {
|
||||||
const [selectedProducts, append, remove] = useProductsStore((state) => [
|
const [selectedProducts, append, remove] = useProductsStore((state) => [
|
||||||
state.selctedProducts,
|
state.selectedProducts,
|
||||||
state.append,
|
state.append,
|
||||||
state.remove,
|
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 {
|
interface ProductsStore {
|
||||||
products: Product[];
|
products: Product[];
|
||||||
selctedProducts: string[];
|
selectedProducts: string[];
|
||||||
append: (code: string) => void;
|
append: (code: string) => void;
|
||||||
remove: (code: string) => void;
|
remove: (code: string) => void;
|
||||||
unselectAll: () => void;
|
unselectAll: () => void;
|
||||||
|
@ -17,14 +17,15 @@ interface ProductsStore {
|
||||||
|
|
||||||
export const useProductsStore = create<ProductsStore>((set, get) => ({
|
export const useProductsStore = create<ProductsStore>((set, get) => ({
|
||||||
products: [],
|
products: [],
|
||||||
selctedProducts: [],
|
selectedProducts: [],
|
||||||
append: (code: string) => set((state) => ({ selctedProducts: [...state.selctedProducts, code] })),
|
append: (code: string) =>
|
||||||
|
set((state) => ({ selectedProducts: [...state.selectedProducts, code] })),
|
||||||
remove: (code: string) =>
|
remove: (code: string) =>
|
||||||
set((state) => ({ selctedProducts: state.selctedProducts.filter((item) => item !== code) })),
|
set((state) => ({ selectedProducts: state.selectedProducts.filter((item) => item !== code) })),
|
||||||
unselectAll: () => set({ selctedProducts: [] }),
|
unselectAll: () => set({ selectedProducts: [] }),
|
||||||
selectAll: () => {
|
selectAll: () => {
|
||||||
const products = pluck("id", get().products);
|
const products = pluck("id", get().products);
|
||||||
const selectedProducts = uniq(concat(get().selctedProducts, products));
|
const selectedProducts = uniq(concat(get().selectedProducts, products));
|
||||||
set({ selctedProducts: selectedProducts });
|
set({ selectedProducts: selectedProducts });
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
import "@mantine/notifications/styles.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
|
@ -9,6 +11,7 @@ import { theme } from "./theme";
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<MantineProvider defaultColorScheme="dark" theme={theme}>
|
<MantineProvider defaultColorScheme="dark" theme={theme}>
|
||||||
|
<Notifications />
|
||||||
<App />
|
<App />
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</React.StrictMode>
|
</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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user