mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-24 07:26:47 +08:00
Fix: Remove antd from dataset-page (#8830)
### What problem does this PR solve? remove antd from dataset-page [#3221](https://github.com/infiniflow/ragflow/issues/3221) ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@ -1,22 +0,0 @@
|
||||
.tweenGroup {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
// width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 2px 20px 0px 4px;
|
||||
height: 26px;
|
||||
font-size: 14px;
|
||||
.textEllipsis();
|
||||
position: relative;
|
||||
:global(.ant-tag-close-icon) {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,24 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { InputRef } from 'antd';
|
||||
import { Input, Tag, theme, Tooltip } from 'antd';
|
||||
import { TweenOneGroup } from 'rc-tween-one';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
import { X } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '../ui/hover-card';
|
||||
import { Input } from '../ui/input';
|
||||
interface EditTagsProps {
|
||||
value?: string[];
|
||||
onChange?: (tags: string[]) => void;
|
||||
}
|
||||
|
||||
const EditTag = ({ value = [], onChange }: EditTagsProps) => {
|
||||
const { token } = theme.useToken();
|
||||
const [inputVisible, setInputVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputVisible) {
|
||||
@ -50,34 +53,66 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => {
|
||||
|
||||
const forMap = (tag: string) => {
|
||||
return (
|
||||
<Tooltip title={tag}>
|
||||
<Tag
|
||||
key={tag}
|
||||
className={styles.tag}
|
||||
closable
|
||||
onClose={(e) => {
|
||||
e.preventDefault();
|
||||
handleClose(tag);
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<HoverCard>
|
||||
<HoverCardContent side="top">{tag}</HoverCardContent>
|
||||
<HoverCardTrigger>
|
||||
<div
|
||||
key={tag}
|
||||
className="w-fit flex items-center justify-center gap-2 border-dashed border px-1 rounded-sm bg-background-card"
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="max-w-80 overflow-hidden text-ellipsis">
|
||||
{tag}
|
||||
</div>
|
||||
<X
|
||||
className="w-4 h-4 text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleClose(tag);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
</HoverCard>
|
||||
);
|
||||
};
|
||||
|
||||
const tagChild = value?.map(forMap);
|
||||
|
||||
const tagPlusStyle: React.CSSProperties = {
|
||||
background: token.colorBgContainer,
|
||||
borderStyle: 'dashed',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inputVisible ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="h-8 bg-background-card"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputConfirm}
|
||||
onKeyDown={(e) => {
|
||||
if (e?.key === 'Enter') {
|
||||
handleInputConfirm();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="dashed"
|
||||
className="w-fit flex items-center justify-center gap-2 bg-background-card"
|
||||
onClick={showInput}
|
||||
style={tagPlusStyle}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</Button>
|
||||
)}
|
||||
{Array.isArray(tagChild) && tagChild.length > 0 && (
|
||||
<TweenOneGroup
|
||||
className={styles.tweenGroup}
|
||||
className="flex gap-2 flex-wrap mt-2"
|
||||
enter={{
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
@ -95,21 +130,6 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => {
|
||||
{tagChild}
|
||||
</TweenOneGroup>
|
||||
)}
|
||||
{inputVisible ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
size="small"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputConfirm}
|
||||
onPressEnter={handleInputConfirm}
|
||||
/>
|
||||
) : (
|
||||
<Tag onClick={showInput} style={tagPlusStyle}>
|
||||
<PlusOutlined />
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
.primitiveImg {
|
||||
display: inline-block;
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.imagePreview {
|
||||
display: block;
|
||||
max-width: 45vw;
|
||||
max-height: 40vh;
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
import { api_host } from '@/utils/api';
|
||||
import { Popover } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './index.less';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
|
||||
interface IImage {
|
||||
id: string;
|
||||
@ -16,7 +14,7 @@ const Image = ({ id, className, ...props }: IImage) => {
|
||||
{...props}
|
||||
src={`${api_host}/document/image/${id}`}
|
||||
alt=""
|
||||
className={classNames(styles.primitiveImg, className)}
|
||||
className={classNames('max-w-[45vw] max-h-[40wh] block', className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -25,11 +23,13 @@ export default Image;
|
||||
|
||||
export const ImageWithPopover = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<Popover
|
||||
placement="left"
|
||||
content={<Image id={id} className={styles.imagePreview}></Image>}
|
||||
>
|
||||
<Image id={id} className={styles.image}></Image>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Image id={id} className="max-h-[100px] inline-block"></Image>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<Image id={id} className="max-w-[100px] object-contain"></Image>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,11 +6,12 @@ type InputProps = React.ComponentProps<'input'> & {
|
||||
iconPosition?: 'left' | 'right';
|
||||
};
|
||||
|
||||
function Input({
|
||||
const Input = function ({
|
||||
className,
|
||||
type,
|
||||
icon,
|
||||
iconPosition = 'left',
|
||||
ref,
|
||||
...props
|
||||
}: InputProps) {
|
||||
return (
|
||||
@ -27,6 +28,7 @@ function Input({
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
@ -45,6 +47,6 @@ function Input({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { Input };
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
@ -27,50 +28,9 @@ import {
|
||||
import { cn } from '@/lib/utils';
|
||||
import { RAGFlowSelectOptionType } from '../ui/select';
|
||||
|
||||
const countries = [
|
||||
{
|
||||
label: 'America',
|
||||
options: [
|
||||
{ value: 'United States', label: '🇺🇸' },
|
||||
{ value: 'Canada', label: '🇨🇦' },
|
||||
{ value: 'Mexico', label: '🇲🇽' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Africa',
|
||||
options: [
|
||||
{ value: 'South Africa', label: '🇿🇦' },
|
||||
{ value: 'Nigeria', label: '🇳🇬' },
|
||||
{ value: 'Morocco', label: '🇲🇦' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Asia',
|
||||
options: [
|
||||
{ value: 'China', label: '🇨🇳' },
|
||||
{ value: 'Japan', label: '🇯🇵' },
|
||||
{ value: 'India', label: '🇮🇳' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Europe',
|
||||
options: [
|
||||
{ value: 'United Kingdom', label: '🇬🇧' },
|
||||
{ value: 'France', label: '🇫🇷' },
|
||||
{ value: 'Germany', label: '🇩🇪' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Oceania',
|
||||
options: [
|
||||
{ value: 'Australia', label: '🇦🇺' },
|
||||
{ value: 'New Zealand', label: '🇳🇿' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export type SelectWithSearchFlagOptionType = {
|
||||
label: string;
|
||||
value?: string;
|
||||
options: RAGFlowSelectOptionType[];
|
||||
};
|
||||
|
||||
@ -84,99 +44,113 @@ export type SelectWithSearchFlagProps = {
|
||||
export const SelectWithSearch = forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
SelectWithSearchFlagProps
|
||||
>(
|
||||
(
|
||||
{ value: val = '', onChange, options = countries, triggerClassName },
|
||||
ref,
|
||||
) => {
|
||||
const id = useId();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [value, setValue] = useState<string>('');
|
||||
>(({ value: val = '', onChange, options = [], triggerClassName }, ref) => {
|
||||
const id = useId();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [value, setValue] = useState<string>('');
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(val: string) => {
|
||||
setValue(val);
|
||||
setOpen(false);
|
||||
onChange?.(val);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSelect = useCallback(
|
||||
(val: string) => {
|
||||
setValue(val);
|
||||
}, [val]);
|
||||
setOpen(false);
|
||||
onChange?.(val);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={id}
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]',
|
||||
triggerClassName,
|
||||
)}
|
||||
>
|
||||
{value ? (
|
||||
<span className="flex min-w-0 options-center gap-2">
|
||||
<span className="text-lg leading-none truncate">
|
||||
{
|
||||
options
|
||||
.map((group) =>
|
||||
group.options.find((item) => item.value === value),
|
||||
)
|
||||
.filter(Boolean)[0]?.label
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Select value</span>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
size={16}
|
||||
className="text-muted-foreground/80 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0"
|
||||
align="start"
|
||||
useEffect(() => {
|
||||
setValue(val);
|
||||
}, [val]);
|
||||
const selectLabel = useMemo(() => {
|
||||
const optionTemp = options[0];
|
||||
if (optionTemp?.options) {
|
||||
return optionTemp.options.find((opt) => opt.value === value)?.label || '';
|
||||
} else {
|
||||
return options.find((opt) => opt.value === value)?.label || '';
|
||||
}
|
||||
}, [options, value]);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id={id}
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]',
|
||||
triggerClassName,
|
||||
)}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search ..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No data found.</CommandEmpty>
|
||||
{options.map((group) => (
|
||||
<Fragment key={group.label}>
|
||||
<CommandGroup heading={group.label}>
|
||||
{group.options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
<span className="text-lg leading-none">
|
||||
{option.label}
|
||||
</span>
|
||||
{value ? (
|
||||
<span className="flex min-w-0 options-center gap-2">
|
||||
<span className="text-lg leading-none truncate">
|
||||
{selectLabel}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Select value</span>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
size={16}
|
||||
className="text-muted-foreground/80 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search ..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No data found.</CommandEmpty>
|
||||
{options.map((group) => {
|
||||
if (group.options) {
|
||||
return (
|
||||
<Fragment key={group.label}>
|
||||
<CommandGroup heading={group.label}>
|
||||
{group.options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
<span className="text-lg leading-none">
|
||||
{option.label}
|
||||
</span>
|
||||
|
||||
{value === option.value && (
|
||||
<CheckIcon size={16} className="ml-auto" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Fragment>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
);
|
||||
{value === option.value && (
|
||||
<CheckIcon size={16} className="ml-auto" />
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<CommandItem
|
||||
key={group.value}
|
||||
value={group.value}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
<span className="text-lg leading-none">{group.label}</span>
|
||||
|
||||
{value === group.value && (
|
||||
<CheckIcon size={16} className="ml-auto" />
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
SelectWithSearch.displayName = 'SelectWithSearch';
|
||||
|
||||
@ -22,6 +22,7 @@ const buttonVariants = cva(
|
||||
tertiary:
|
||||
'bg-colors-background-sentiment-solid-primary text-colors-text-persist-light hover:bg-colors-background-sentiment-solid-primary/80',
|
||||
icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80',
|
||||
dashed: 'border border-dashed border-input hover:bg-accent',
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 px-2.5 py-1.5 ',
|
||||
@ -49,7 +50,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
className={cn(
|
||||
'bg-background-card',
|
||||
buttonVariants({ variant, size, className }),
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
64
web/src/components/ui/divider.tsx
Normal file
64
web/src/components/ui/divider.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
// src/components/ui/divider.tsx
|
||||
import React from 'react';
|
||||
|
||||
type Direction = 'horizontal' | 'vertical';
|
||||
type DividerType = 'horizontal' | 'vertical' | 'text';
|
||||
|
||||
interface DividerProps {
|
||||
direction?: Direction;
|
||||
type?: DividerType;
|
||||
text?: React.ReactNode;
|
||||
color?: string;
|
||||
margin?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Divider: React.FC<DividerProps> = ({
|
||||
direction = 'horizontal',
|
||||
type = 'horizontal',
|
||||
text,
|
||||
color = 'border-muted-foreground/50',
|
||||
margin = 'my-4',
|
||||
className = '',
|
||||
}) => {
|
||||
const baseClasses = 'flex items-center';
|
||||
const directionClass = direction === 'horizontal' ? 'flex-row' : 'flex-col';
|
||||
const colorClass = color.startsWith('border-') ? color : `border-${color}`;
|
||||
const marginClass = margin || '';
|
||||
const textClass = 'px-4 text-sm text-muted-foreground';
|
||||
|
||||
// Default vertical style
|
||||
if (direction === 'vertical') {
|
||||
return (
|
||||
<div
|
||||
className={`h-full ${colorClass} border-l ${marginClass} ${className}`}
|
||||
>
|
||||
{type === 'text' && (
|
||||
<div className="transform -rotate-90 px-2 whitespace-nowrap">
|
||||
{text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Horizontal with text
|
||||
if (type === 'text') {
|
||||
return (
|
||||
<div
|
||||
className={`${baseClasses} ${directionClass} ${marginClass} ${className}`}
|
||||
>
|
||||
<div className={`flex-1 ${colorClass} border-t`}></div>
|
||||
<div className={textClass}>{text}</div>
|
||||
<div className={`flex-1 ${colorClass} border-t`}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default horizontal
|
||||
return (
|
||||
<div className={`${colorClass} border-t ${marginClass} ${className}`} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Divider;
|
||||
@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-64 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 origin-[--radix-hover-card-content-transform-origin]',
|
||||
'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap 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 origin-[--radix-hover-card-content-transform-origin]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
199
web/src/components/ui/modal.tsx
Normal file
199
web/src/components/ui/modal.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
// src/components/ui/modal.tsx
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { Loader, X } from 'lucide-react';
|
||||
import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
title?: ReactNode;
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
className?: string;
|
||||
size?: 'small' | 'default' | 'large';
|
||||
closable?: boolean;
|
||||
closeIcon?: ReactNode;
|
||||
maskClosable?: boolean;
|
||||
destroyOnClose?: boolean;
|
||||
full?: boolean;
|
||||
confirmLoading?: boolean;
|
||||
cancelText?: ReactNode | string;
|
||||
okText?: ReactNode | string;
|
||||
onOk?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export const Modal: FC<ModalProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
children,
|
||||
footer,
|
||||
className = '',
|
||||
size = 'default',
|
||||
closable = true,
|
||||
closeIcon = <X className="w-4 h-4" />,
|
||||
maskClosable = true,
|
||||
destroyOnClose = false,
|
||||
full = false,
|
||||
onOk,
|
||||
onCancel,
|
||||
confirmLoading,
|
||||
cancelText,
|
||||
okText,
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
small: 'max-w-md',
|
||||
default: 'max-w-2xl',
|
||||
large: 'max-w-4xl',
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
// Handle ESC key close
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && maskClosable) {
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [maskClosable, onOpenChange]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
onOpenChange?.(false);
|
||||
onCancel?.();
|
||||
}, [onOpenChange, onCancel]);
|
||||
|
||||
const handleOk = useCallback(() => {
|
||||
onOpenChange?.(true);
|
||||
onOk?.();
|
||||
}, [onOpenChange, onOk]);
|
||||
const handleChange = (open: boolean) => {
|
||||
onOpenChange?.(open);
|
||||
if (open) {
|
||||
handleOk();
|
||||
}
|
||||
if (!open) {
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
const footEl = useMemo(() => {
|
||||
let footerTemp;
|
||||
if (footer) {
|
||||
footerTemp = footer;
|
||||
} else {
|
||||
footerTemp = (
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCancel()}
|
||||
className="px-2 py-1 border border-input rounded-md hover:bg-muted"
|
||||
>
|
||||
{cancelText ?? t('modal.cancelText')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={confirmLoading}
|
||||
onClick={() => handleOk()}
|
||||
className="px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
||||
>
|
||||
{confirmLoading && (
|
||||
<Loader className="inline-block mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{okText ?? t('modal.okText')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center justify-end border-t border-border px-6 py-4">
|
||||
{footerTemp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [footer, cancelText, t, confirmLoading, okText, handleCancel, handleOk]);
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={handleChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
className="fixed inset-0 z-50 bg-colors-background-neutral-weak/50 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
onClick={() => maskClosable && onOpenChange?.(false)}
|
||||
>
|
||||
<DialogPrimitive.Content
|
||||
className={`relative w-[700px] ${full ? 'max-w-full' : sizeClasses[size]} ${className} bg-colors-background-neutral-standard rounded-lg shadow-lg transition-all`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* title */}
|
||||
{title && (
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
<DialogPrimitive.Title className="text-lg font-medium text-foreground">
|
||||
{title}
|
||||
</DialogPrimitive.Title>
|
||||
{closable && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted"
|
||||
>
|
||||
{closeIcon}
|
||||
</button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[80vh]">
|
||||
{destroyOnClose && !open ? null : children}
|
||||
</div>
|
||||
|
||||
{/* footer */}
|
||||
{footEl}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Overlay>
|
||||
</DialogPrimitive.Portal>
|
||||
</DialogPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
// example usage
|
||||
/*
|
||||
import { Modal } from '@/components/ui/modal';
|
||||
|
||||
function Demo() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setOpen(true)}>open modal</button>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title="title"
|
||||
footer={
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setOpen(false)} className="px-4 py-2 border rounded-md">
|
||||
cancel
|
||||
</button>
|
||||
<button onClick={() => setOpen(false)} className="px-4 py-2 bg-primary text-white rounded-md">
|
||||
ok
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="py-4">弹窗内容区域</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={'modal-title'}
|
||||
onOk={handleOk}
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
>
|
||||
<div className="py-4">弹窗内容区域</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
57
web/src/components/ui/space.tsx
Normal file
57
web/src/components/ui/space.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
// src/components/ui/space.tsx
|
||||
import React from 'react';
|
||||
|
||||
type Direction = 'horizontal' | 'vertical';
|
||||
type Size = 'small' | 'middle' | 'large';
|
||||
|
||||
interface SpaceProps {
|
||||
direction?: Direction;
|
||||
size?: Size;
|
||||
align?: string;
|
||||
justify?: string;
|
||||
wrap?: boolean;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const sizeClasses: Record<Size, string> = {
|
||||
small: 'gap-2',
|
||||
middle: 'gap-4',
|
||||
large: 'gap-8',
|
||||
};
|
||||
|
||||
const directionClasses: Record<Direction, string> = {
|
||||
horizontal: 'flex-row',
|
||||
vertical: 'flex-col',
|
||||
};
|
||||
|
||||
const Space: React.FC<SpaceProps> = ({
|
||||
direction = 'horizontal',
|
||||
size = 'middle',
|
||||
align,
|
||||
justify,
|
||||
wrap = false,
|
||||
className = '',
|
||||
children,
|
||||
}) => {
|
||||
const baseClasses = 'flex';
|
||||
const directionClass = directionClasses[direction];
|
||||
const sizeClass = sizeClasses[size];
|
||||
const alignClass = align ? `items-${align}` : '';
|
||||
const justifyClass = justify ? `justify-${justify}` : '';
|
||||
const wrapClass = wrap ? 'flex-wrap' : '';
|
||||
|
||||
const classes = [
|
||||
baseClasses,
|
||||
directionClass,
|
||||
sizeClass,
|
||||
alignClass,
|
||||
justifyClass,
|
||||
wrapClass,
|
||||
className,
|
||||
].join(' ');
|
||||
|
||||
return <div className={classes}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Space;
|
||||
@ -1,51 +1,105 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
ComponentProps,
|
||||
FocusEventHandler,
|
||||
forwardRef,
|
||||
TextareaHTMLAttributes,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
interface TextareaProps
|
||||
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'autoSize'> {
|
||||
autoSize?: {
|
||||
minRows?: number;
|
||||
maxRows?: number;
|
||||
};
|
||||
}
|
||||
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, autoSize, ...props }, ref) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const getLineHeight = (element: HTMLElement): number => {
|
||||
const style = window.getComputedStyle(element);
|
||||
return parseInt(style.lineHeight, 10) || 20;
|
||||
};
|
||||
const adjustHeight = useCallback(() => {
|
||||
if (!textareaRef.current) return;
|
||||
const lineHeight = getLineHeight(textareaRef.current);
|
||||
const maxHeight = (autoSize?.maxRows || 3) * lineHeight;
|
||||
textareaRef.current.style.height = 'auto';
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<'textarea'>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-colors-background-inverse-weak px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
if (!textareaRef.current) return;
|
||||
|
||||
const scrollHeight = textareaRef.current.scrollHeight;
|
||||
textareaRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
|
||||
});
|
||||
}, [autoSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoSize) {
|
||||
adjustHeight();
|
||||
}
|
||||
}, [textareaRef, autoSize, adjustHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof ref === 'function') {
|
||||
ref(textareaRef.current);
|
||||
} else if (ref) {
|
||||
ref.current = textareaRef.current;
|
||||
}
|
||||
}, [ref]);
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-colors-background-inverse-weak px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
rows={autoSize?.minRows ?? props.rows ?? undefined}
|
||||
style={{
|
||||
maxHeight: autoSize?.maxRows
|
||||
? `${autoSize.maxRows * 20}px`
|
||||
: undefined,
|
||||
overflow: autoSize ? 'auto' : undefined,
|
||||
}}
|
||||
ref={textareaRef}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
|
||||
type Value = string | readonly string[] | number | undefined;
|
||||
|
||||
export const BlurTextarea = React.forwardRef<
|
||||
export const BlurTextarea = forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<'textarea'> & {
|
||||
ComponentProps<'textarea'> & {
|
||||
value: Value;
|
||||
onChange(value: Value): void;
|
||||
}
|
||||
>(({ value, onChange, ...props }, ref) => {
|
||||
const [val, setVal] = React.useState<Value>();
|
||||
const [val, setVal] = useState<Value>();
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> =
|
||||
React.useCallback((e) => {
|
||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(e) => {
|
||||
setVal(e.target.value);
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleBlur: React.FocusEventHandler<HTMLTextAreaElement> =
|
||||
React.useCallback(
|
||||
(e) => {
|
||||
onChange?.(e.target.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
const handleBlur: FocusEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(e) => {
|
||||
onChange?.(e.target.value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setVal(value);
|
||||
}, [value]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user