diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e187f99f..fe052de2 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -21,10 +21,12 @@
"@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8",
"@antv/x6-react-shape": "^2.2.3",
+ "@hookform/resolvers": "^3.9.1",
"@logicflow/core": "^2.0.9",
"@logicflow/extension": "^2.0.13",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
@@ -43,9 +45,11 @@
"react": "^18.2.0",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "^18.2.0",
+ "react-hook-form": "^7.54.2",
"react-redux": "^9.0.4",
"react-router-dom": "^6.21.0",
- "recharts": "^2.15.0"
+ "recharts": "^2.15.0",
+ "zod": "^3.24.1"
},
"devDependencies": {
"@types/dagre": "^0.7.52",
@@ -1659,6 +1663,15 @@
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
"license": "MIT"
},
+ "node_modules/@hookform/resolvers": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmmirror.com/@hookform/resolvers/-/resolvers-3.9.1.tgz",
+ "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -2145,6 +2158,29 @@
}
}
},
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.1.tgz",
+ "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
@@ -7429,6 +7465,22 @@
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
+ "node_modules/react-hook-form": {
+ "version": "7.54.2",
+ "resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.54.2.tgz",
+ "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
@@ -9020,6 +9072,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zod": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmmirror.com/zod/-/zod-3.24.1.tgz",
+ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/zustand": {
"version": "4.5.5",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.5.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 2e064cc1..b09a4394 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,10 +23,12 @@
"@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8",
"@antv/x6-react-shape": "^2.2.3",
+ "@hookform/resolvers": "^3.9.1",
"@logicflow/core": "^2.0.9",
"@logicflow/extension": "^2.0.13",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
@@ -45,9 +47,11 @@
"react": "^18.2.0",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "^18.2.0",
+ "react-hook-form": "^7.54.2",
"react-redux": "^9.0.4",
"react-router-dom": "^6.21.0",
- "recharts": "^2.15.0"
+ "recharts": "^2.15.0",
+ "zod": "^3.24.1"
},
"devDependencies": {
"@types/dagre": "^0.7.52",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 75e66a2c..e4a269b6 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -47,6 +47,9 @@ importers:
'@antv/x6-react-shape':
specifier: ^2.2.3
version: 2.2.3(@antv/x6@2.18.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@hookform/resolvers':
+ specifier: ^3.9.1
+ version: 3.9.1(react-hook-form@7.54.2(react@18.3.1))
'@logicflow/core':
specifier: ^2.0.9
version: 2.0.9
@@ -59,6 +62,9 @@ importers:
'@radix-ui/react-avatar':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-label':
+ specifier: ^2.1.1
+ version: 2.1.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-progress':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -113,6 +119,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
+ react-hook-form:
+ specifier: ^7.54.2
+ version: 7.54.2(react@18.3.1)
react-redux:
specifier: ^9.0.4
version: 9.2.0(@types/react@18.3.16)(react@18.3.1)(redux@5.0.1)
@@ -122,6 +131,9 @@ importers:
recharts:
specifier: ^2.15.0
version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ zod:
+ specifier: ^3.24.1
+ version: 3.24.1
devDependencies:
'@types/dagre':
specifier: ^0.7.52
@@ -718,6 +730,11 @@ packages:
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
+ '@hookform/resolvers@3.9.1':
+ resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+
'@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
engines: {node: '>=10.10.0'}
@@ -913,6 +930,19 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-label@2.1.1':
+ resolution: {integrity: sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-popper@1.2.1':
resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==}
peerDependencies:
@@ -2872,6 +2902,12 @@ packages:
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+ react-hook-form@7.54.2:
+ resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -3414,6 +3450,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zod@3.24.1:
+ resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
+
zustand@4.5.5:
resolution: {integrity: sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==}
engines: {node: '>=12.7.0'}
@@ -4099,6 +4138,10 @@ snapshots:
'@floating-ui/utils@0.2.8': {}
+ '@hookform/resolvers@3.9.1(react-hook-form@7.54.2(react@18.3.1))':
+ dependencies:
+ react-hook-form: 7.54.2(react@18.3.1)
+
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@@ -4288,6 +4331,15 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.16
+ '@radix-ui/react-label@2.1.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.16
+ '@types/react-dom': 18.3.5(@types/react@18.3.16)
+
'@radix-ui/react-popper@1.2.1(@types/react-dom@18.3.5(@types/react@18.3.16))(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -6419,6 +6471,10 @@ snapshots:
react-fast-compare@3.2.2: {}
+ react-hook-form@7.54.2(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
react-is@16.13.1: {}
react-is@18.3.1: {}
@@ -6949,6 +7005,8 @@ snapshots:
yocto-queue@0.1.0: {}
+ zod@3.24.1: {}
+
zustand@4.5.5(@types/react@18.3.16)(immer@10.1.1)(react@18.3.1):
dependencies:
use-sync-external-store: 1.2.2(react@18.3.1)
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
index 487e37a3..8349c74b 100644
--- a/frontend/public/favicon.svg
+++ b/frontend/public/favicon.svg
@@ -1,8 +1,8 @@
\ No newline at end of file
diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx
new file mode 100644
index 00000000..f6afdaf2
--- /dev/null
+++ b/frontend/src/components/ui/form.tsx
@@ -0,0 +1,176 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx
new file mode 100644
index 00000000..683faa79
--- /dev/null
+++ b/frontend/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/frontend/src/pages/Login/index.module.css b/frontend/src/pages/Login/index.module.css
index 9b0c0974..24ea3a87 100644
--- a/frontend/src/pages/Login/index.module.css
+++ b/frontend/src/pages/Login/index.module.css
@@ -1,62 +1,84 @@
.loginContainer {
- height: 100vh;
- width: 100vw;
- display: flex;
- justify-content: center;
- align-items: center;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ display: flex;
+}
+
+.leftSection {
+ flex: 1;
+ background-color: black;
+ color: white;
+ padding: 2.5rem;
+ display: none;
+ flex-direction: column;
+ justify-content: space-between;
+}
+
+@media (min-width: 1024px) {
+ .leftSection {
+ display: flex;
+ }
+}
+
+.rightSection {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ background-color: white;
}
.loginBox {
- width: 400px;
- padding: 40px;
- background: rgba(255, 255, 255, 0.9);
- border-radius: 16px;
- box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
- backdrop-filter: blur(4px);
+ width: 100%;
+ max-width: 400px;
}
.logo {
- text-align: center;
- margin-bottom: 40px;
-}
-
-.logo img {
- width: 64px;
- height: 64px;
- margin-bottom: 16px;
+ text-align: center;
+ margin-bottom: 2rem;
}
.logo h1 {
- font-size: 24px;
- color: #333;
- margin: 0;
+ font-size: 1.875rem;
+ font-weight: 600;
+ color: #000;
}
.input {
- height: 50px;
- border-radius: 8px;
- border: 1px solid #e8e8e8;
- transition: all 0.3s;
+ width: 100%;
+ height: 2.5rem;
+ border-radius: 0.375rem;
+ border: 1px solid #e2e8f0;
+ padding: 0.5rem 1rem;
+ margin-bottom: 1rem;
+ transition: all 0.2s;
+}
+
+.input:hover {
+ border-color: #cbd5e0;
}
-.input:hover,
.input:focus {
- border-color: #764ba2;
- box-shadow: 0 0 0 2px rgba(118, 75, 162, 0.2);
+ border-color: #3182ce;
+ box-shadow: 0 0 0 1px #3182ce;
}
.loginButton {
- height: 50px;
- border-radius: 8px;
- font-size: 16px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- border: none;
- margin-top: 16px;
- transition: all 0.3s;
+ width: 100%;
+ height: 2.5rem;
+ background-color: #000;
+ color: white;
+ border: none;
+ border-radius: 0.375rem;
+ font-weight: 500;
+ transition: all 0.2s;
}
.loginButton:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(118, 75, 162, 0.4);
+ background-color: #1a1a1a;
+}
+
+.loginButton:disabled {
+ background-color: #e2e8f0;
+ cursor: not-allowed;
}
\ No newline at end of file
diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx
index a2f8f1d2..7009c63e 100644
--- a/frontend/src/pages/Login/index.tsx
+++ b/frontend/src/pages/Login/index.tsx
@@ -75,63 +75,91 @@ const Login: React.FC = () => {
return (
-
-
-
管理系统
+ {/* 左侧区域 */}
+
+
+
Deploy Ease Platform
-
-
+
+
+ "这个平台帮助我们显著提升了部署效率,让团队可以更专注于业务开发。"
+
+
张工
+
高级运维工程师
+
+
-
+
+
+
管理系统
+
+
-
-
-
-
-
-
-
+
);