mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-04 14:36:54 +08:00
feat: refactor select model
This commit is contained in:
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
export default function Btn(props: {
|
||||
export interface BtnProps {
|
||||
onClick?: () => void;
|
||||
icon?: JSX.Element;
|
||||
type?: ButtonType;
|
||||
@@ -14,7 +14,9 @@ export default function Btn(props: {
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
}) {
|
||||
}
|
||||
|
||||
export default function Btn(props: BtnProps) {
|
||||
const {
|
||||
onClick,
|
||||
icon,
|
||||
|
@@ -1,136 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog";
|
||||
import Warning from "@/app/icons/warning.svg";
|
||||
import Btn from "@/app/components/Btn";
|
||||
|
||||
interface ConfirmProps {
|
||||
onOk?: () => Promise<void> | void;
|
||||
onCancel?: () => void;
|
||||
okText: string;
|
||||
cancelText: string;
|
||||
content: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
const baseZIndex = 150;
|
||||
|
||||
const Confirm = (props: ConfirmProps) => {
|
||||
const { visible, onOk, onCancel, okText, cancelText, content, title } = props;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const mergeOpen = visible ?? open;
|
||||
|
||||
return (
|
||||
<AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay
|
||||
className="bg-confirm-mask fixed inset-0 animate-mask "
|
||||
style={{ zIndex: baseZIndex - 1 }}
|
||||
/>
|
||||
<AlertDialog.Content
|
||||
className={`
|
||||
fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%]
|
||||
`}
|
||||
style={{ zIndex: baseZIndex - 1 }}
|
||||
>
|
||||
<div className="flex-1"> </div>
|
||||
<div
|
||||
className={`flex flex-col
|
||||
bg-confirm-panel text-confirm-mask
|
||||
md:p-6 md:w-confirm md:gap-6 md:rounded-lg
|
||||
`}
|
||||
>
|
||||
<AlertDialog.Title
|
||||
className={`
|
||||
flex items-center justify-start gap-3 font-common
|
||||
md:text-chat-header-title md:font-bold md:leading-5
|
||||
`}
|
||||
>
|
||||
<Warning />
|
||||
{title}
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description
|
||||
className={`
|
||||
font-common font-normal
|
||||
md:text-sm-title md:leading-[158%]
|
||||
`}
|
||||
>
|
||||
{content}
|
||||
</AlertDialog.Description>
|
||||
<div
|
||||
style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}
|
||||
>
|
||||
<AlertDialog.Cancel asChild>
|
||||
<Btn
|
||||
className={`
|
||||
md:px-4 md:py-2.5 bg-delete-chat-cancel-btn border border-delete-chat-cancel-btn text-text-delete-chat-cancel-btn rounded-md
|
||||
`}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onCancel?.();
|
||||
}}
|
||||
text={cancelText}
|
||||
/>
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action asChild>
|
||||
<Btn
|
||||
className={`
|
||||
md:px-4 md:py-2.5 bg-delete-chat-ok-btn text-text-delete-chat-ok-btn rounded-md
|
||||
`}
|
||||
onClick={() => {
|
||||
const toDo = onOk?.();
|
||||
if (toDo instanceof Promise) {
|
||||
toDo.then(() => {
|
||||
setOpen(false);
|
||||
});
|
||||
} else {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
text={okText}
|
||||
/>
|
||||
</AlertDialog.Action>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1"> </div>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.id = "confirm-root";
|
||||
div.style.height = "0px";
|
||||
document.body.appendChild(div);
|
||||
|
||||
const show = (props: Omit<ConfirmProps, "visible" | "onCancel" | "onOk">) => {
|
||||
const root = createRoot(div);
|
||||
const closeModal = () => {
|
||||
root.unmount();
|
||||
};
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
root.render(
|
||||
<Confirm
|
||||
{...props}
|
||||
visible={true}
|
||||
onCancel={() => {
|
||||
closeModal();
|
||||
resolve(false);
|
||||
}}
|
||||
onOk={() => {
|
||||
closeModal();
|
||||
resolve(true);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Confirm.show = show;
|
||||
|
||||
export default Confirm;
|
@@ -55,11 +55,11 @@ export default function Input(props: CommonInputProps & InputProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-[100%] rounded-chat-input bg-input flex gap-3 items-center px-3 py-2 ${className}`}
|
||||
className={` group/input w-[100%] rounded-chat-input bg-input flex gap-3 items-center px-3 py-2 ${className} hover:bg-select-hover`}
|
||||
>
|
||||
<input
|
||||
{...rest}
|
||||
className=" overflow-hidden text-text-input text-sm-title leading-input outline-none flex-1"
|
||||
className=" overflow-hidden text-text-input text-sm-title leading-input outline-none flex-1 group-hover/input:bg-input-input-ele-hover"
|
||||
type={internalType}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
|
261
app/components/Modal/index.tsx
Normal file
261
app/components/Modal/index.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React, { useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import * as AlertDialog from "@radix-ui/react-alert-dialog";
|
||||
import Btn, { BtnProps } from "@/app/components/Btn";
|
||||
|
||||
import Warning from "@/app/icons/warning.svg";
|
||||
import Close from "@/app/icons/closeIcon.svg";
|
||||
|
||||
export interface ModalProps {
|
||||
onOk?: () => void;
|
||||
onCancel?: () => void;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
okBtnProps?: BtnProps;
|
||||
cancelBtnProps?: BtnProps;
|
||||
content?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
visible?: boolean;
|
||||
noFooter?: boolean;
|
||||
noHeader?: boolean;
|
||||
isMobile?: boolean;
|
||||
closeble?: boolean;
|
||||
type?: "modal" | "bottom-drawer";
|
||||
headerBordered?: boolean;
|
||||
}
|
||||
|
||||
export interface WarnProps
|
||||
extends Omit<
|
||||
ModalProps,
|
||||
| "closeble"
|
||||
| "isMobile"
|
||||
| "noHeader"
|
||||
| "noFooter"
|
||||
| "onOk"
|
||||
| "okBtnProps"
|
||||
| "cancelBtnProps"
|
||||
> {
|
||||
onOk?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
const baseZIndex = 150;
|
||||
|
||||
const Modal = (props: ModalProps) => {
|
||||
const {
|
||||
onOk,
|
||||
onCancel,
|
||||
okText,
|
||||
cancelText,
|
||||
content,
|
||||
title,
|
||||
visible,
|
||||
noFooter,
|
||||
noHeader,
|
||||
closeble = true,
|
||||
okBtnProps,
|
||||
cancelBtnProps,
|
||||
type = "modal",
|
||||
headerBordered,
|
||||
} = props;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const mergeOpen = visible ?? open;
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
let layoutClassName = "";
|
||||
let panelClassName = "";
|
||||
let titleClassName = "";
|
||||
let footerClassName = "";
|
||||
|
||||
switch (type) {
|
||||
case "bottom-drawer":
|
||||
layoutClassName =
|
||||
"fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] md:";
|
||||
panelClassName = "";
|
||||
titleClassName = "";
|
||||
footerClassName = "";
|
||||
break;
|
||||
case "modal":
|
||||
default:
|
||||
layoutClassName =
|
||||
"fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] max-sm:w-modal-modal-type-mobile";
|
||||
panelClassName = "rounded-lg px-6 sm:w-modal-modal-type";
|
||||
titleClassName = "py-6 max-sm:pb-3";
|
||||
footerClassName = "py-6";
|
||||
}
|
||||
const btnCommonClass = "px-4 py-2.5 rounded-md max-sm:flex-1";
|
||||
const { className: okBtnClass } = okBtnProps || {};
|
||||
const { className: cancelBtnClass } = cancelBtnProps || {};
|
||||
|
||||
return (
|
||||
<AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay
|
||||
className="bg-modal-mask fixed inset-0 animate-mask "
|
||||
style={{ zIndex: baseZIndex - 1 }}
|
||||
/>
|
||||
<AlertDialog.Content
|
||||
className={`
|
||||
${layoutClassName}
|
||||
`}
|
||||
style={{ zIndex: baseZIndex - 1 }}
|
||||
>
|
||||
<div className="flex-1"> </div>
|
||||
<div
|
||||
className={`flex flex-col
|
||||
bg-moda-panel text-modal-mask
|
||||
${headerBordered ? " border-b border-modal-header-bottom" : ""}
|
||||
${panelClassName}
|
||||
`}
|
||||
>
|
||||
{!noHeader && (
|
||||
<AlertDialog.Title
|
||||
className={`
|
||||
flex items-center justify-between gap-3 font-common
|
||||
md:text-chat-header-title md:font-bold md:leading-5
|
||||
${titleClassName}
|
||||
`}
|
||||
>
|
||||
<div className="flex gap-3 justify-start flex-1 items-center">
|
||||
{title}
|
||||
</div>
|
||||
{closeble && (
|
||||
<div
|
||||
className="items-center"
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<Close />
|
||||
</div>
|
||||
)}
|
||||
</AlertDialog.Title>
|
||||
)}
|
||||
{content}
|
||||
{!noFooter && (
|
||||
<div
|
||||
className={`
|
||||
flex gap-3 sm:justify-end max-sm:justify-between
|
||||
${footerClassName}
|
||||
`}
|
||||
>
|
||||
<AlertDialog.Cancel asChild>
|
||||
<Btn
|
||||
{...cancelBtnProps}
|
||||
onClick={() => handleClose()}
|
||||
text={cancelText}
|
||||
className={`${btnCommonClass} ${cancelBtnClass}`}
|
||||
/>
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action asChild>
|
||||
<Btn
|
||||
{...okBtnProps}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onOk?.();
|
||||
}}
|
||||
text={okText}
|
||||
className={`${btnCommonClass} ${okBtnClass}`}
|
||||
/>
|
||||
</AlertDialog.Action>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1"> </div>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export const Warn = ({
|
||||
title,
|
||||
onOk,
|
||||
visible,
|
||||
content,
|
||||
...props
|
||||
}: WarnProps) => {
|
||||
const [internalVisible, setVisible] = useState(visible);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={
|
||||
<>
|
||||
<Warning />
|
||||
{title}
|
||||
</>
|
||||
}
|
||||
content={
|
||||
<AlertDialog.Description
|
||||
className={`
|
||||
font-common font-normal
|
||||
md:text-sm-title md:leading-[158%]
|
||||
`}
|
||||
>
|
||||
{content}
|
||||
</AlertDialog.Description>
|
||||
}
|
||||
closeble={false}
|
||||
onOk={() => {
|
||||
const toDo = onOk?.();
|
||||
if (toDo instanceof Promise) {
|
||||
toDo.then(() => {
|
||||
setVisible(false);
|
||||
});
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
}}
|
||||
visible={internalVisible}
|
||||
okBtnProps={{
|
||||
className: `bg-delete-chat-ok-btn text-text-delete-chat-ok-btn `,
|
||||
}}
|
||||
cancelBtnProps={{
|
||||
className: `bg-delete-chat-cancel-btn border border-delete-chat-cancel-btn text-text-delete-chat-cancel-btn`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.id = "confirm-root";
|
||||
div.style.height = "0px";
|
||||
document.body.appendChild(div);
|
||||
|
||||
Modal.warn = (props: Omit<WarnProps, "visible" | "onCancel" | "onOk">) => {
|
||||
const root = createRoot(div);
|
||||
const closeModal = () => {
|
||||
root.unmount();
|
||||
};
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
root.render(
|
||||
<Warn
|
||||
{...props}
|
||||
visible={true}
|
||||
onCancel={() => {
|
||||
closeModal();
|
||||
resolve(false);
|
||||
}}
|
||||
onOk={() => {
|
||||
closeModal();
|
||||
resolve(true);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const Trigger = (props: ModalProps) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
Modal.Trigger = Trigger;
|
||||
|
||||
export default Modal;
|
@@ -1,5 +1,12 @@
|
||||
import useRelativePosition from "@/app/hooks/useRelativePosition";
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
RefObject,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
const ArrowIcon = ({ sibling }: { sibling: RefObject<HTMLDivElement> }) => {
|
||||
@@ -55,6 +62,7 @@ export interface PopoverProps {
|
||||
noArrow?: boolean;
|
||||
delayClose?: number;
|
||||
useGlobalRoot?: boolean;
|
||||
getPopoverPanelRef?: (ref: RefObject<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export default function Popover(props: PopoverProps) {
|
||||
@@ -70,6 +78,7 @@ export default function Popover(props: PopoverProps) {
|
||||
noArrow = false,
|
||||
delayClose = 0,
|
||||
useGlobalRoot,
|
||||
getPopoverPanelRef,
|
||||
} = props;
|
||||
|
||||
const [internalShow, setShow] = useState(false);
|
||||
@@ -175,10 +184,14 @@ export default function Popover(props: PopoverProps) {
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const closeTimer = useRef<number>(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
getPopoverPanelRef?.(popoverRef);
|
||||
onShow?.(internalShow);
|
||||
}, [internalShow]);
|
||||
|
||||
if (trigger === "click") {
|
||||
const handleOpen = (e: { currentTarget: any }) => {
|
||||
clearTimeout(closeTimer.current);
|
||||
onShow?.(true);
|
||||
setShow(true);
|
||||
getRelativePosition(e.currentTarget, "");
|
||||
window.document.documentElement.style.overflow = "hidden";
|
||||
@@ -186,11 +199,9 @@ export default function Popover(props: PopoverProps) {
|
||||
const handleClose = () => {
|
||||
if (delayClose) {
|
||||
closeTimer.current = window.setTimeout(() => {
|
||||
onShow?.(false);
|
||||
setShow(false);
|
||||
}, delayClose);
|
||||
} else {
|
||||
onShow?.(false);
|
||||
setShow(false);
|
||||
}
|
||||
window.document.documentElement.style.overflow = "auto";
|
||||
@@ -219,7 +230,7 @@ export default function Popover(props: PopoverProps) {
|
||||
)}
|
||||
{createPortal(
|
||||
<div
|
||||
className={`${popoverCommonClass} ${popoverClassName} cursor-pointer`}
|
||||
className={`${popoverCommonClass} ${popoverClassName} cursor-pointer overflow-auto`}
|
||||
style={{ zIndex: baseZIndex + 1, ...placementStyle }}
|
||||
ref={popoverRef}
|
||||
>
|
||||
|
@@ -58,14 +58,22 @@ const Select = <Value extends number | string>(props: SearchProps<Value>) => {
|
||||
{options?.map((o) => (
|
||||
<div
|
||||
key={o.value}
|
||||
className={`flex items-center p-3 gap-2 rounded-action-btn hover:bg-select-option-hovered cursor-pointer`}
|
||||
className={`flex items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered cursor-pointer`}
|
||||
onClick={() => {
|
||||
onSelect?.(o.value);
|
||||
}}
|
||||
>
|
||||
{!!o.icon && <div className="">{o.icon}</div>}
|
||||
<div className={`flex-1`}>{o.label}</div>
|
||||
{selectedOption?.value === o.value && <Selected />}
|
||||
<div className="flex gap-2 flex-1">
|
||||
{!!o.icon && <div className="">{o.icon}</div>}
|
||||
<div className={`flex-1`}>{o.label}</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
selectedOption?.value === o.value ? "opacity-100" : "opacity-0"
|
||||
}
|
||||
>
|
||||
<Selected />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -86,7 +94,7 @@ const Select = <Value extends number | string>(props: SearchProps<Value>) => {
|
||||
className={selectClassName}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-3 py-2 px-3 bg-select rounded-action-btn font-time text-sm-title cursor-pointer`}
|
||||
className={`flex items-center gap-3 py-2 px-3 bg-select rounded-action-btn font-time text-sm-title cursor-pointer hover:bg-select-hover`}
|
||||
ref={contentRef}
|
||||
>
|
||||
<div className={`flex items-center gap-2 flex-1`}>
|
||||
|
Reference in New Issue
Block a user