This commit is contained in:
2026-02-14 02:31:51 +08:00
parent fde8721583
commit 8de453811b
33 changed files with 4415 additions and 228 deletions

View 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>
);
}

View 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>
);
};

View 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>
);
};

View 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} />;
};

View 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,
},
};
};

View 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
View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};