This commit is contained in:
dengqichen 2025-01-10 10:30:19 +08:00
parent a6d4d9cc5a
commit 70f19f230d
5 changed files with 485 additions and 204 deletions

View File

@ -31,6 +31,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
@ -45,6 +46,7 @@
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"antd": "^5.22.2", "antd": "^5.22.2",
"axios": "^1.6.2", "axios": "^1.6.2",
"cmdk": "^1.0.4",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"form-render": "^2.5.1", "form-render": "^2.5.1",
"less": "^4.2.1", "less": "^4.2.1",
@ -2096,6 +2098,7 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/primitive": "1.1.1", "@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1",
@ -2352,6 +2355,43 @@
} }
} }
}, },
"node_modules/@radix-ui/react-popover": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.4.tgz",
"integrity": "sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.3",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.1",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "^2.6.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": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
@ -4397,6 +4437,22 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cmdk": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/cmdk/-/cmdk-1.0.4.tgz",
"integrity": "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-id": "^1.1.0",
"@radix-ui/react-primitive": "^2.0.0",
"use-sync-external-store": "^1.2.2"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/color": { "node_modules/color": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz", "resolved": "https://registry.npmmirror.com/color/-/color-3.2.1.tgz",

View File

@ -33,6 +33,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
@ -47,6 +48,7 @@
"ajv-formats": "^3.0.1", "ajv-formats": "^3.0.1",
"antd": "^5.22.2", "antd": "^5.22.2",
"axios": "^1.6.2", "axios": "^1.6.2",
"cmdk": "^1.0.4",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"form-render": "^2.5.1", "form-render": "^2.5.1",
"less": "^4.2.1", "less": "^4.2.1",

View File

@ -0,0 +1,152 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { type DialogProps } from "@radix-ui/react-dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -35,6 +35,11 @@ import {useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod"; import {zodResolver} from "@hookform/resolvers/zod";
import {applicationFormSchema, type ApplicationFormValues} from "../schema"; import {applicationFormSchema, type ApplicationFormValues} from "../schema";
import {Textarea} from "@/components/ui/textarea"; import {Textarea} from "@/components/ui/textarea";
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem} from "@/components/ui/command";
import {ScrollArea} from "@/components/ui/scroll-area";
import {Check} from "lucide-react";
import {cn} from "@/lib/utils";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
interface ApplicationModalProps { interface ApplicationModalProps {
open: boolean; open: boolean;
@ -165,221 +170,258 @@ const ApplicationModal: React.FC<ApplicationModalProps> = ({
return ( return (
<Dialog open={open} onOpenChange={(open) => !open && onCancel()}> <Dialog open={open} onOpenChange={(open) => !open && onCancel()}>
<DialogContent className="sm:max-w-[600px]"> <DialogContent className="sm:max-w-[600px] h-[90vh]">
<DialogHeader> <DialogHeader className="pb-4">
<DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle> <DialogTitle>{isEdit ? '编辑' : '新建'}</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <ScrollArea className="flex-1 -mx-6">
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4"> <div className="px-6">
<FormField <Form {...form}>
control={form.control} <form id="applicationForm" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
name="appCode" <FormField
render={({field}) => ( control={form.control}
<FormItem> name="appCode"
<FormLabel></FormLabel> render={({field}) => (
<FormControl> <FormItem>
<Input <FormLabel></FormLabel>
{...field} <FormControl>
disabled={isEdit} <Input
placeholder="请输入应用编码" {...field}
/> disabled={isEdit}
</FormControl> placeholder="请输入应用编码"
<FormMessage/> />
</FormItem> </FormControl>
)} <FormMessage/>
/> </FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
render={({field}) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
placeholder="请输入应用名称" placeholder="请输入应用名称"
/> />
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="language" name="language"
render={({field}) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<Select <Select
disabled={isEdit} disabled={isEdit}
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="请选择开发语言"/> <SelectValue placeholder="请选择开发语言"/>
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value={DevelopmentLanguageTypeEnum.JAVA}>Java</SelectItem> <SelectItem value={DevelopmentLanguageTypeEnum.JAVA}>Java</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.NODE_JS}>NodeJS</SelectItem> <SelectItem value={DevelopmentLanguageTypeEnum.NODE_JS}>NodeJS</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.PYTHON}>Python</SelectItem> <SelectItem value={DevelopmentLanguageTypeEnum.PYTHON}>Python</SelectItem>
<SelectItem value={DevelopmentLanguageTypeEnum.GO}>Go</SelectItem> <SelectItem value={DevelopmentLanguageTypeEnum.GO}>Go</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="gitInstanceId" name="gitInstanceId"
render={({field}) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel>Git实例</FormLabel> <FormLabel>Git实例</FormLabel>
<Select <Select
disabled={isEdit} disabled={isEdit}
onValueChange={(value) => { onValueChange={(value) => {
field.onChange(Number(value)); field.onChange(Number(value));
handleGitInstanceChange(Number(value)); handleGitInstanceChange(Number(value));
}} }}
value={field.value?.toString()} value={field.value?.toString()}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="请选择Git实例"/> <SelectValue placeholder="请选择Git实例"/>
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{gitInstances.map(instance => ( {gitInstances.map(instance => (
<SelectItem key={instance.id} value={instance.id.toString()}> <SelectItem key={instance.id} value={instance.id.toString()}>
{instance.name} {instance.name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="repositoryGroupId" name="repositoryGroupId"
render={({field}) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<Select <Popover>
onValueChange={(value) => field.onChange(Number(value))} <PopoverTrigger asChild>
value={field.value?.toString()} <FormControl>
disabled={!form.watch('gitInstanceId')} <Button
> variant="outline"
<FormControl> role="combobox"
<SelectTrigger> disabled={!form.watch('gitInstanceId')}
<SelectValue placeholder="请选择代码仓库组"/> className={cn(
</SelectTrigger> "w-full justify-between",
</FormControl> !field.value && "text-muted-foreground"
<SelectContent> )}
{repositoryGroups.map((group) => ( >
<SelectItem key={group.id} value={group.id.toString()}> {field.value
{group.name} ? repositoryGroups.find(
</SelectItem> (group) => group.id === field.value
))} )?.name
</SelectContent> : "请选择代码仓库组"}
</Select> </Button>
<FormMessage/> </FormControl>
</FormItem> </PopoverTrigger>
)} <PopoverContent className="w-full p-0">
/> <Command>
<CommandInput placeholder="搜索代码仓库组..." />
<CommandEmpty></CommandEmpty>
<CommandGroup>
<ScrollArea className="h-60">
{repositoryGroups.map((group) => (
<CommandItem
value={group.name}
key={group.id}
onSelect={() => {
form.setValue("repositoryGroupId", group.id);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
group.id === field.value
? "opacity-100"
: "opacity-0"
)}
/>
{group.name}
</CommandItem>
))}
</ScrollArea>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<FormMessage/>
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="repoUrl" name="repoUrl"
render={({field}) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<FormControl> <FormControl>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
{...field} {...field}
placeholder={selectedGitInstance ? `例如: ${selectedGitInstance.url}/your-project.git` : "请先选择Git实例"} placeholder={selectedGitInstance ? `例如: ${selectedGitInstance.url}/your-project.git` : "请先选择Git实例"}
disabled={!selectedGitInstance} disabled={!selectedGitInstance}
/> />
</div> </div>
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="appDesc" name="appDesc"
render={({field}) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea
{...field} {...field}
placeholder="请输入应用描述" placeholder="请输入应用描述"
rows={4} rows={4}
/> />
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="enabled" name="enabled"
render={({field}) => ( render={({field}) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4"> <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel></FormLabel> <FormLabel></FormLabel>
</div> </div>
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="sort" name="sort"
render={({field}) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel></FormLabel> <FormLabel></FormLabel>
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
type="number" type="number"
min={0} min={0}
onChange={(e) => field.onChange(Number(e.target.value))} onChange={(e) => field.onChange(Number(e.target.value))}
/> />
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter> <div className="h-4" />
<Button type="button" variant="outline" onClick={onCancel}> </form>
</Form>
</Button> </div>
<Button type="submit"> </ScrollArea>
<DialogFooter className="pt-4">
</Button> <Button type="button" variant="outline" onClick={onCancel}>
</DialogFooter>
</form> </Button>
</Form> <Button type="submit" form="applicationForm">
</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );