Skip to content

Commit

Permalink
chore: optimize evm example
Browse files Browse the repository at this point in the history
  • Loading branch information
ByteZhang1024 committed Nov 5, 2024
1 parent 3e41c75 commit d349624
Show file tree
Hide file tree
Showing 27 changed files with 3,903 additions and 187 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type ApiPayloadAction =
| { type: 'SET_PRESUPPOSE_PARAMS'; payload: IPresupposeParam[] };

// 优化 JSON 格式化函数
const tryFormatJson = (json: string) => {
export const tryFormatJson = (json: string) => {
try {
return JSON.stringify(JSON.parse(json), null, 2);
} catch {
Expand Down
87 changes: 87 additions & 0 deletions packages/example/components/ApiForm/ApiButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { memo, useContext, useCallback } from 'react';
import { useAtom } from 'jotai';
import { Button } from '../ui/button';
import { ApiFormContext } from './ApiForm';
import { useValidation } from './hooks/useValidation';
import { get } from 'lodash';
import { toast } from '../ui/use-toast';

export interface ApiButtonProps {
id: string;
label: string;
onClick: () => Promise<void>;
validation?: {
fields: string[];
validator?: (values: Record<string, { id: string; value: string; required: boolean }>) => string | undefined;
};
}

export const ApiButton = memo(({
id,
label,
onClick,
validation
}: ApiButtonProps) => {
const context = useContext(ApiFormContext);
if (!context) throw new Error('ApiButton must be used within ApiForm');

const { store } = context;
const [field, setField] = useAtom(store.fieldsAtom(id));

const loading = field.extra?.loading ?? false;
const result = field.extra?.result;

const setResult = (value: string) => {
setField({ ...field, extra: { ...field.extra, result: value } });
};

const setLoading = (value: boolean) => {
setField({ ...field, extra: { ...field.extra, loading: value } });
};

const { validate } = useValidation({
store,
validation
});

const handleClick = useCallback(async () => {
setResult(undefined);

const { isValid, error } = validate();
if (!isValid) {
setResult(error || '验证失败');
return;
}

try {
setLoading(true);
await onClick();
} catch (error) {
const errorMessage = get(error, 'message', 'error') ?? JSON.stringify(error);
toast({
title: '执行失败',
description: errorMessage,
variant: 'destructive',
});
setResult(errorMessage);
} finally {
setLoading(false);
}
}, [onClick, validate, setLoading, setResult]);

return (
<div className="flex flex-col gap-1">
<Button
key={id}
onClick={handleClick}
disabled={loading}
loading={loading}
>
{label}
</Button>
{result && <div className="text-red-500 text-sm">{result}</div>}
</div>
);
});

ApiButton.displayName = 'ApiButton';
52 changes: 52 additions & 0 deletions packages/example/components/ApiForm/ApiCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { memo, useContext, useEffect } from 'react';
import { useAtom } from 'jotai';
import { ApiFormContext } from './ApiForm';
import { Checkbox } from '../ui/checkbox';

export interface ApiCheckboxProps {
id: string;
label?: string;
defaultChecked?: boolean;
}

export const ApiCheckbox = memo(({
id,
label,
defaultChecked
}: ApiCheckboxProps) => {
const context = useContext(ApiFormContext);
if (!context) throw new Error('ApiField must be used within ApiForm');

const { store } = context;
const [field, setField] = useAtom(store.fieldsAtom(id));

useEffect(() => {
if (defaultChecked) {
setField({ ...field, value: defaultChecked });
}
}, []);

return <div className="flex items-center gap-2">
<Checkbox
id={id}
defaultChecked={defaultChecked}
required={field.required}
checked={field.value}
onCheckedChange={(e) => setField({ ...field, value: e })}
disabled={field.disabled}
/>

<label
htmlFor={id}
className="p-0 m-0 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{label}
</label>

{field.error && (
<div className="text-sm text-red-500">{field.error}</div>
)}
</div>
});

ApiCheckbox.displayName = 'ApiCheckbox';
159 changes: 159 additions & 0 deletions packages/example/components/ApiForm/ApiCombobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo } from 'react';
import { useAtom } from 'jotai';
import { ApiFormContext } from './ApiForm';
import { Label } from '../ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Button } from '../ui/button';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command';
import { ChevronDownIcon, CheckIcon } from 'lucide-react';
import { cn } from '../../lib/utils';

interface IOption<T> {
value: string;
label: string;
extra?: T;
remark?: string;
}

export interface ApiComboboxProps<T = any> {
id: string;
label?: string;
required?: boolean;
defaultValue?: string;
placeholder?: string;
onRequestOptions?: () => Promise<IOption<T>[]>
onValueChange?: (value: string | null) => void;
onOptionChange?: (option: IOption<T> | null) => void;
}

export interface ApiComboboxRef<T = any> {
getCurrentValue: () => string | undefined;
getCurrentOption: () => IOption<T> | undefined;
getOptions: () => IOption<T>[];
setValue: (value: string) => void;
setOptions: (options: IOption<T>[]) => void;
}

export const ApiCombobox = forwardRef<ApiComboboxRef, ApiComboboxProps>(function ApiCombobox<T = any>(
{
id,
label,
required,
defaultValue,
placeholder,
onRequestOptions,
onValueChange,
onOptionChange
}: ApiComboboxProps<T>,
ref: React.Ref<ApiComboboxRef<T>>,
) {
const context = useContext(ApiFormContext);
if (!context) throw new Error('ApiField must be used within ApiForm');

const { store } = context;
const [field, setField] = useAtom(store.fieldsAtom(id));
const options = field.extra?.options as IOption<T>[] | undefined;
const [open, setOpen] = React.useState(false)

const getCurrentOption = useCallback(() => {
return options?.find(opt => opt.value === field.value);
}, [options, field.value]);

const currentOption = useMemo(() => getCurrentOption(), [getCurrentOption]);

const setOptions = useCallback((options: IOption<T>[]) => {
setField({
...field, extra: {
options
}
});
}, [setField]);

useEffect(() => {
if (onRequestOptions) {
onRequestOptions().then((options) => {
setOptions(options);
});
}
}, [onRequestOptions]);

const setValue = useCallback((value: string | null) => {
setField({ ...field, value });
setOpen(false)
onValueChange?.(value);
onOptionChange?.(options?.find(opt => opt.value === value) ?? null);
}, [setField, onValueChange, onOptionChange, options]);

useEffect(() => {
if (defaultValue) {
setField({ ...field, value: defaultValue });
}
}, []);

useImperativeHandle(ref, () => ({
setValue,
getCurrentValue: () => currentOption?.value,
getCurrentOption: () => currentOption,
getOptions: () => options,
setOptions,
}), [currentOption]);

return <div className='flex flex-col'>
{label && (
<Label htmlFor={id}>
{label}
{required && <span className="text-red-500">*</span>}
</Label>
)}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{currentOption?.value
? options.find((option) => option.value === currentOption?.value)?.label
: placeholder}
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder={placeholder} className="h-9" />
<CommandList>
<CommandEmpty>没有找到选项</CommandEmpty>
<CommandGroup>
{options?.map((option) => (
<CommandItem
key={option.value}
value={option.label}
onSelect={(currentLabel) => {
const currentOption = options?.find(opt => opt.label === currentLabel);
setValue(currentOption?.value ?? null);
}}
>
{option.label}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
currentOption?.value === option.value ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{currentOption?.remark && (
<span className="px-1 text-sm text-muted-foreground">
{currentOption.remark}
</span>
)}
</div>
});

ApiCombobox.displayName = 'ApiCombobox';
81 changes: 81 additions & 0 deletions packages/example/components/ApiForm/ApiField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { memo, useContext, useEffect } from 'react';
import { useAtom } from 'jotai';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { ApiFormContext } from './ApiForm';


interface ApiInputProps {
id: string;
placeholder?: string;
type?: 'text' | 'number' | 'password';
hidden?: boolean;
defaultValue?: string;
}

const ApiInput = memo(({
id,
placeholder,
type = 'text',
hidden = false,
defaultValue
}: ApiInputProps) => {

const context = useContext(ApiFormContext);
if (!context) throw new Error('ApiField must be used within ApiForm');

const { store } = context;
const [field, setField] = useAtom(store.fieldsAtom(id));

useEffect(() => {
if (defaultValue) {
setField({ ...field, value: defaultValue });
}
}, []);

return <>
<Input
id={id}
value={field.value}
defaultValue={defaultValue}
onChange={(e) => setField({ ...field, value: e.target.value })}
placeholder={placeholder}
disabled={field.disabled}
type={type}
hidden={hidden}
/>
{field.error && !hidden && (
<div className="text-sm text-red-500">{field.error}</div>
)}
</>
});

export interface ApiFieldProps extends ApiInputProps {
id: string;
label?: string;
required?: boolean;
}

export const ApiField = memo(({
id,
label,
placeholder,
required,
hidden = false,
defaultValue
}: ApiFieldProps) => {

return (
<div>
{label && !hidden && (
<Label htmlFor={id}>
{label}
{required && <span className="text-red-500">*</span>}
</Label>
)}
<ApiInput id={id} placeholder={placeholder} hidden={hidden} defaultValue={defaultValue} />
</div>
);
});

ApiField.displayName = 'ApiField';
Loading

0 comments on commit d349624

Please sign in to comment.