generated from kevisual/vite-react-template
temp
This commit is contained in:
80
src/components/a/auto-complate.tsx
Normal file
80
src/components/a/auto-complate.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/a/button';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
|
||||
type Option = {
|
||||
value?: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type AutoComplateProps = {
|
||||
options: Option[];
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
width?: string;
|
||||
};
|
||||
export function AutoComplate(props: AutoComplateProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, _setValue] = React.useState('');
|
||||
const setValue = (value: string) => {
|
||||
props?.onChange?.(value);
|
||||
_setValue(value);
|
||||
};
|
||||
const showLabel = React.useMemo(() => {
|
||||
const option = props.options.find((option) => option.value === value);
|
||||
if (option) {
|
||||
return option?.label;
|
||||
}
|
||||
if (props.value) return props.value;
|
||||
if (value) return value;
|
||||
return 'Select ...';
|
||||
}, [value, props.value]);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger >
|
||||
<Button variant='outline' role='combobox' aria-expanded={open} className={cn(props.width ? props.width : 'w-[400px]', 'justify-between')}>
|
||||
{showLabel}
|
||||
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={cn(props.width ? props.width : 'w-[400px]', ' p-0')}>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={props.placeholder ?? 'Search options...'}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === 'Enter') {
|
||||
setOpen(false);
|
||||
const value = e.target?.value || '';
|
||||
setValue(value.trim());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No options found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{props.options.map((framework) => (
|
||||
<CommandItem
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? '' : currentValue);
|
||||
setOpen(false);
|
||||
}}>
|
||||
<Check className={cn('mr-2 h-4 w-4', value === framework.value ? 'opacity-100' : 'opacity-0')} />
|
||||
{framework.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
19
src/components/a/button.tsx
Normal file
19
src/components/a/button.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Button as UiButton } from '@/components/ui/button';
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cn } from '@/lib/utils';
|
||||
export const IconButton: typeof UiButton = (props) => {
|
||||
return <UiButton variant='ghost' size='icon' {...props} className={cn('h-8 w-8 cursor-pointer', props?.className)} />;
|
||||
};
|
||||
|
||||
export const Button = (props: Parameters<typeof UiButton>[0]) => {
|
||||
return <UiButton variant='ghost' {...props} className={cn('cursor-pointer', props?.className)} />;
|
||||
};
|
||||
|
||||
export const ButtonTextIcon = (props: ButtonPrimitive.Props & { icon: React.ReactNode }) => {
|
||||
return (
|
||||
<UiButton variant={'outline'} size='sm' {...props} className={cn('cursor-pointer flex items-center gap-2', props?.className)}>
|
||||
{props.icon}
|
||||
{props.children}
|
||||
</UiButton>
|
||||
);
|
||||
};
|
||||
102
src/components/a/confirm.tsx
Normal file
102
src/components/a/confirm.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type useConfirmOptions = {
|
||||
confrimProps: ConfirmProps;
|
||||
};
|
||||
type Fn = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
export const useConfirm = (opts?: useConfirmOptions) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
type ConfirmOptions = {
|
||||
onOk?: Fn;
|
||||
onCancel?: Fn;
|
||||
};
|
||||
const confirm = (opts?: ConfirmOptions) => {
|
||||
setOpen(true);
|
||||
};
|
||||
const module = useMemo(() => {
|
||||
return <Confirm {...opts?.confrimProps} hasTrigger={false} open={open} setOpen={setOpen} />;
|
||||
}, [open]);
|
||||
return {
|
||||
module: module,
|
||||
open,
|
||||
setOpen,
|
||||
confirm,
|
||||
};
|
||||
};
|
||||
|
||||
type ConfirmProps = {
|
||||
children?: React.ReactNode;
|
||||
tip?: React.ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
onOkText?: string;
|
||||
onCancelText?: string;
|
||||
onOk?: Fn;
|
||||
onCancle?: Fn;
|
||||
hasTrigger?: boolean;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
footer?: React.ReactNode;
|
||||
};
|
||||
export const Confirm = (props: ConfirmProps) => {
|
||||
const [isOpen, setIsOpen] = useState(props.open);
|
||||
const hasTrigger = props.hasTrigger ?? true;
|
||||
useEffect(() => {
|
||||
setIsOpen(props.open);
|
||||
}, [props.open]);
|
||||
return (
|
||||
<AlertDialog
|
||||
open={isOpen}
|
||||
onOpenChange={(v) => {
|
||||
setIsOpen(v);
|
||||
props?.setOpen?.(v);
|
||||
}}>
|
||||
{hasTrigger && (
|
||||
<>
|
||||
{props?.children && <AlertDialogTrigger>{props?.children ?? props?.tip ?? '提示'}</AlertDialogTrigger>}
|
||||
{!props?.children && <AlertDialogTrigger>{props?.tip ?? '提示'}</AlertDialogTrigger>}
|
||||
</>
|
||||
)}
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{props?.title ?? '是否确认删除?'}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{props?.description ?? '此操作无法撤销,是否继续。'}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
{props?.footer && <div className='flex gap-2'>{props?.footer}</div>}
|
||||
{!props?.footer && (
|
||||
<>
|
||||
<AlertDialogCancel
|
||||
className='cursor-pointer'
|
||||
onClick={(e) => {
|
||||
props?.onCancle?.(e);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
{props?.onCancelText ?? '取消'}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className='cursor-pointer'
|
||||
onClick={(e) => {
|
||||
props?.onOk?.(e);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
{props?.onOkText ?? '确定'}
|
||||
</AlertDialogAction>
|
||||
</>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
21
src/components/a/divider.tsx
Normal file
21
src/components/a/divider.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
export const Divider = (props: { orientation?: 'horizontal' | 'vertical' }) => {
|
||||
const { orientation = 'horizontal' } = props;
|
||||
|
||||
const dividerStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
backgroundColor: '#e5e7eb', // 淡灰色分割线
|
||||
...(orientation === 'horizontal'
|
||||
? {
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
}
|
||||
: {
|
||||
alignSelf: 'stretch',
|
||||
width: '1px',
|
||||
display: 'inline-block',
|
||||
margin: '2px 4px',
|
||||
}),
|
||||
};
|
||||
|
||||
return <div style={dividerStyle} role='separator' aria-orientation={orientation} />;
|
||||
};
|
||||
132
src/components/a/drag-modal/index.tsx
Normal file
132
src/components/a/drag-modal/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Draggable from 'react-draggable';
|
||||
import { cn as clsxMerge } from '@/lib/utils';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
type DragModalProps = {
|
||||
title?: React.ReactNode;
|
||||
content?: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
containerClassName?: string;
|
||||
handleClassName?: string;
|
||||
contentClassName?: string;
|
||||
focus?: boolean;
|
||||
/**
|
||||
* 默认大小, 单位为px
|
||||
* width: defaultSize.width || 320
|
||||
* height: defaultSize.height || 400
|
||||
*/
|
||||
defaultSize?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
export const DragModal = (props: DragModalProps) => {
|
||||
const dragRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
nodeRef={dragRef as any}
|
||||
onStop={(e, data) => {
|
||||
// console.log(e, data);
|
||||
}}
|
||||
handle='.handle'
|
||||
grid={[1, 1]}
|
||||
scale={1}
|
||||
bounds='parent'
|
||||
defaultPosition={{
|
||||
x: 0,
|
||||
y: 0,
|
||||
}}>
|
||||
<div
|
||||
className={clsxMerge('absolute top-0 left-0 bg-white rounded-md border border-gray-200 shadow-sm pointer-events-auto', props.focus ? 'z-30' : '', props.containerClassName)}
|
||||
ref={dragRef}
|
||||
style={props.style}>
|
||||
<div className={clsxMerge('handle cursor-move border-b border-gray-200 py-2 px-4', props.handleClassName)}>{props.title || 'Move'}</div>
|
||||
<Resizable
|
||||
className={clsxMerge('', props.contentClassName)}
|
||||
defaultSize={{
|
||||
width: props.defaultSize?.width || 600,
|
||||
height: props.defaultSize?.height || 400,
|
||||
}}
|
||||
onResizeStop={(e, direction, ref, d) => {
|
||||
// console.log(e, direction, ref, d);
|
||||
}}
|
||||
enable={{
|
||||
bottom: true,
|
||||
right: true,
|
||||
bottomRight: true,
|
||||
}}>
|
||||
{props.content}
|
||||
</Resizable>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
type DragModalTitleProps = {
|
||||
title?: React.ReactNode;
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
};
|
||||
export const DragModalTitle = (props: DragModalTitleProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsxMerge('flex flex-row items-center justify-between', props.className)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick?.();
|
||||
}}>
|
||||
<div className='text-sm font-medium text-gray-700'>
|
||||
{props.title}
|
||||
{props.children}
|
||||
</div>
|
||||
<div
|
||||
className='text-gray-500 cursor-pointer p-2 hover:bg-gray-100 rounded-md'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClose?.();
|
||||
}}>
|
||||
<X className='w-4 h-4 ' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getComputedHeight = () => {
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth;
|
||||
return { height, width };
|
||||
};
|
||||
|
||||
export const useComputedHeight = () => {
|
||||
const [computedHeight, setComputedHeight] = useState({
|
||||
height: 0,
|
||||
width: 0,
|
||||
});
|
||||
useEffect(() => {
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth;
|
||||
setComputedHeight({ height, width });
|
||||
}, []);
|
||||
return computedHeight;
|
||||
};
|
||||
|
||||
export const useDragSize = (width = 600, heigth = 400) => {
|
||||
const computedHeight = getComputedHeight();
|
||||
const isMin = computedHeight.width < width;
|
||||
return {
|
||||
defaultSize: {
|
||||
width: isMin ? computedHeight.width : 600,
|
||||
height: 400,
|
||||
},
|
||||
style: {
|
||||
left: isMin ? 0 : computedHeight.width / 2 - width / 2,
|
||||
top: computedHeight.height / 2 - heigth / 2,
|
||||
},
|
||||
};
|
||||
};
|
||||
92
src/components/a/input.tsx
Normal file
92
src/components/a/input.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Input as UIInput } from '@/components/ui/input';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
export type InputProps = { label?: string } & React.ComponentProps<'input'>;
|
||||
export const Input = (props: InputProps) => {
|
||||
return <UIInput {...props} />;
|
||||
};
|
||||
|
||||
type TagsInputProps = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
label?: React.ReactNode;
|
||||
showLabel?: boolean;
|
||||
options?: string[]; // 可选,暂未实现自动补全
|
||||
};
|
||||
|
||||
export const TagsInput = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
label = '',
|
||||
showLabel = false,
|
||||
}: TagsInputProps) => {
|
||||
const [input, setInput] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setInput('');
|
||||
}, [value]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(e.target.value);
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
(e.key === 'Enter' || e.key === ',' || e.key === 'Tab') &&
|
||||
input.trim()
|
||||
) {
|
||||
e.preventDefault();
|
||||
const newTag = input.trim();
|
||||
if (!value.includes(newTag)) {
|
||||
onChange([...value, newTag]);
|
||||
}
|
||||
setInput('');
|
||||
} else if (e.key === 'Backspace' && !input && value.length) {
|
||||
onChange(value.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (idx: number) => {
|
||||
onChange(value.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showLabel && label && (
|
||||
<label className="block mb-1 text-sm font-medium text-gray-700">{label}</label>
|
||||
)}
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2 border rounded px-2 py-1 min-h-[40px] focus-within:ring-2 focus-within:ring-blue-500 bg-white"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
{value.map((tag, idx) => (
|
||||
<span
|
||||
key={tag + idx}
|
||||
className="flex items-center bg-blue-100 text-blue-800 rounded px-2 py-0.5 text-sm mr-1 mb-1"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 text-blue-500 hover:text-blue-700 focus:outline-none"
|
||||
onClick={() => handleRemoveTag(idx)}
|
||||
aria-label="Remove tag"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="flex-1 min-w-[80px] border-none outline-none bg-transparent py-1 text-sm"
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
src/components/a/menu.tsx
Normal file
39
src/components/a/menu.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuItem,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useEffect, useState } from 'react';
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
options?: { label: string; value: string }[];
|
||||
onSelect?: (value: string) => void;
|
||||
};
|
||||
|
||||
export const Menu = (props: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedValue, setSelectedValue] = useState('');
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
setSelectedValue(value);
|
||||
props.onSelect?.(value);
|
||||
setOpen(false);
|
||||
};
|
||||
const showSelectedValue = selectedValue || 'Select an option';
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger className={props.className}>{props.children ? props.children : showSelectedValue}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{props.options?.map((option) => (
|
||||
<DropdownMenuItem key={option.value} onSelect={() => handleSelect(option.value)}>
|
||||
{option.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
24
src/components/a/modal.tsx
Normal file
24
src/components/a/modal.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
|
||||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
type ModalProps = {
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => any;
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const { open = false, setOpen, title, children } = props;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
33
src/components/a/select.tsx
Normal file
33
src/components/a/select.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Select as UISelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
label?: string;
|
||||
};
|
||||
type SelectProps = {
|
||||
className?: string;
|
||||
options?: Option[];
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (value: any) => any;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
};
|
||||
export const Select = (props: SelectProps) => {
|
||||
const options = props.options || [];
|
||||
return (
|
||||
<UISelect onValueChange={props.onChange} value={props.value}>
|
||||
<SelectTrigger className='w-[180px]'>
|
||||
<SelectValue placeholder={props.placeholder || '请选择'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((item, index) => {
|
||||
return (
|
||||
<SelectItem key={index} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</UISelect>
|
||||
);
|
||||
};
|
||||
15
src/components/a/tooltip.tsx
Normal file
15
src/components/a/tooltip.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Tooltip as UITooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import React from 'react';
|
||||
|
||||
export const Tooltip = (props: { children?: React.ReactNode; title?: React.ReactNode; placement?: 'top' | 'bottom' | 'left' | 'right' }) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<UITooltip>
|
||||
<TooltipTrigger>{props.children}</TooltipTrigger>
|
||||
<TooltipContent side={props.placement || 'top'}>
|
||||
<p>{props.title}</p>
|
||||
</TooltipContent>
|
||||
</UITooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user